SpringSecurity Oauth2 jwt

  • SpringSecurity Oauth2 jwt
    • 1 用户认证分析
      • 1.1 单点登录
      • 1.2 第三方账号登录
    • 2 认证解决方案
      • 2.1 单点登录技术方案
      • 2.2 第三方登录技术方案
        • 2.2.1 Oauth2认证流程
        • 2.2.2 Oauth2在项目的应用
      • 2.3 Spring security Oauth2认证解决方案
    • 3 Jwt令牌回顾
      • 3.1 令牌结构
      • 3.2 生成私钥公钥
      • 3.3 基于私钥生成jwt令牌
        • 3.3.1导入认证服务
        • 3.3.2 认证服务中创建测试类
      • 3.4 基于公钥解析jwt令牌
    • 4 Oauth2.0入门
      • 4.1 准备工作
      • 4.2 Oauth2授权模式介绍
        • 4.2.1 授权码模式
          • 4.2.1.1 授权码授权流程
          • 4.2.1.2 申请授权码
          • 4.2.1.3 申请令牌
          • 4.2.1.4 令牌校验
          • 4.2.1.5 刷新令牌
        • 4.2.2 密码模式
          • 4.2.2.1 申请令牌
      • 4.3 资源服务授权
        • 4.3.1 用户服务对接Oauth2
        • 4.3.2 资源服务授权测试
    • 5 认证开发
      • 5.1 需求分析
      • 5.2 Redis配置
      • 5.3 认证服务
        • 5.3.1 认证需求分析
        • 5.3.2 授权参数配置
        • 5.3.3 申请令牌测试
        • 5.3.4 业务层
        • 5.3.5 控制层
        • 5.3.6 登录请求放行
        • 5.3.7 测试认证接口
        • 5.3.8 动态获取用户信息
          • 5.3.8.1 定义被访问接口
          • 5.3.8.2 放行该接口,修改ResourceServerConfig类
          • 5.3.8.3定义feign接口
          • 5.3.8.4 认证服务添加依赖
          • 5.3.8.5 修改认证服务启动类
          • 5.3.8.6 修改用户认证类
    • 6 认证服务对接网关
      • 6.1 新建网关工程changgou_gateway_web
      • 6.2 网关全局过滤器
    • 7 自定义登录页面
      • 7.1 认证服务添加依赖
      • 7.2 资源\成品页面\登录页面
      • 7.3 把页面放到下面的项目中
      • 7.4 静态资源放行
      • 7.5 LoginRedirectController
      • 7.6 修改登录页面实现用户登录
      • 7.7 定义login方法
      • 7.8 测试
      • 8权限控制
    • oauth全部代码
    • gateway_web

SpringSecurity Oauth2 jwt

1 用户认证分析

上面流程图描述了用户要操作的各个微服务,用户查看个人信息需要访问客户微服务,下单需要访问订单微服务,秒杀抢购商品需要访问秒杀微服务。每个服务都需要认证用户的身份,身份认证成功后,需要识别用户的角色然后授权访问对应的功能。

1.1 单点登录

用户访问的项目中,至少有3个微服务需要识别用户身份,如果用户访问每个微服务都登录一次就太麻烦了,为了提高用户的体验,我们需要实现让用户在一个系统中登录,其他任意受信任的系统都可以访问,这个功能就叫单点登录。

单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统

1.2 第三方账号登录

随着国内及国外巨头们的平台开放战略以及移动互联网的发展,第三方登录已经不是一个陌生的产品设计概念了。 所谓的第三方登录,是说基于用户在第三方平台上已有的账号和密码来快速完成己方应用的登录或者注册的功能。而这里的第三方平台,一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。

2 认证解决方案

2.1 单点登录技术方案

分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如: MySQL、Redis,考虑性能要求,通常存储在Redis中,如下图:

Java中有很多用户认证的框架都可以实现单点登录:

 1、Apache Shiro. 2、CAS 3、Spring security

2.2 第三方登录技术方案

2.2.1 Oauth2认证流程

​ 第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的
接口协议。
​ OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认
证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP、JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。互联网很多服务如Open API,很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。
参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
Oauth协议:https://tools.ietf.org/html/rfc6749
下边分析一个Oauth2认证的例子,黑马程序员网站使用微信认证的过程:

  1. 用户访问第三方资源,第三方请求微信服务器发送授权请求给用户
  2. 用户确认授权后,返回授权码给第三方
  3. 第三方拿到授权码后,携带授权码向微信申请令牌
  4. 第三方拿到令牌后,携带令牌向微信请求用户的基本信息
  5. 微信资源服务器根据访问令牌,返回给第三方用户的基本信息
    3-5的交互过程用户看不到,用户只能看到登录成功

Oauth2.0认证流程如下: 引自Oauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749

Oauth2包括以下角色:

1、客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:畅购Android客户端、畅购Web客户端(浏览器端)、微信客户端等。

2、资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。

3、授权服务器(也称认证服务器) 用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。

4、资源服务器 存储资源的服务器,比如,畅购用户管理服务器存储了畅购的用户信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。

2.2.2 Oauth2在项目的应用

Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用Oauth2,项目中使用Oauth2可以实现实现如下功能:

1、本系统访问第三方系统的资源

2、外部系统访问本系统的资源

3、本系统前端(客户端) 访问本系统后端微服务的资源。

4、本系统微服务之间访问资源,例如:微服务A访问微服务B的资源,B访问A的资源。

2.3 Spring security Oauth2认证解决方案

本项目采用 Spring security + Oauth2+JWT完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了Oauth2协议,下图是项目认证架构图:

上面的是Oauth2(第三方登录的流程)这里是账号密码登录的流程

这里微服务网关是一个,这里只是因为流程的关系分开画了,然后在认证服务中会链接数据库查询一些用户基本信息放入token中,这样访问各个服务的时候用户信息就显示了

  1. 账号密码登录,网关对于登录和一些静态资源是放行的,可以访问。然后这里是验证登录和生成令牌的过程,这里采用jwt的方式经过RSA的私钥生成令牌,公钥去解析。

  2. 在认证服务的过程中会去链接数据库,访问两个数据库,一个是通过fegin的远程调用访问user,role,perssion表(都是多对多),这样返回的时候就会携带用户和权限信息,一个是oauth_client_details(配置数据库链接,自动访问)拿到第三方授权的账号密码(自关联)。有用户名密码查询正确,把uid和生成令牌的jti形成键值对放入cookie中,redis存放的是jti和对应的token信息,这样就能通过cookie中的uid找到jti,再找到redis中的token可以进行解析了

  3. 当用户访问服务的时候,会经过微服务网关,这里有两种配置解析的方式。

1.公钥存放在网关,判断cookie,redis中的指定值是否存在,最后通过公钥去解析令牌是否合法,如果都没问题就放行

2.如果私钥配置在微服务上的话,网关判断cookie和redis非空后,设置一下请求的响应头

request.mutate().header(“Authorization”,"Bearer "+jwt);

这就是令牌的传递,这样其他微服务就不需要再去cookie和redis去找令牌了,过来的时候就携带了

现在就分为认证服务和资源服务

登录授权后如果有改变权限或者增加微服务地址怎么动态的增加呢?
可以在后台维护一张字典表,专门用作配置,比如要令牌资源路径,或者订单状态,维护一对多的两张表,保存在DB中,取完保存在redis中,用Quartz定时组件来完成定时的更新。

3 Jwt令牌回顾

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。

官网:https://jwt.io/

标准:https://tools.ietf.org/html/rfc7519

JWT令牌的优点:

1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

1、JWT令牌较长,占存储空间比较大。

3.1 令牌结构

通过学习JWT令牌结构为自定义jwt令牌打好基础。

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

一个例子如下:

下边是Header部分的内容

{"alg": "HS256","typ": "JWT"
}

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

一个例子:

{"sub": "1234567890","name": "456","admin": true
}

Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。

一个例子:

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

3.2 生成私钥公钥

JWT令牌生成采用非对称加密算法

1、生成密钥证书 下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

keytool -genkeypair -alias changgou -keyalg RSA -keypass changgou -keystore changgou.jks -storepass changgou

Keytool 是一个java提供的证书管理工具

-alias:密钥的别名
-keyalg:使用的hash算法
-keypass:密钥的访问密码
-keystore:密钥库文件名,changgou.jks保存了生成的证书
-storepass:密钥库的访问密码

查询证书信息:

keytool -list -keystore changgou.jks

2、导出公钥

openssl是一个加解密工具包,这里使用openssl来导出公钥信息。

安装 openssl:http://slproweb.com/products/Win32OpenSSL.html

安装资料目录下的Win64OpenSSL-1_1_1b.exe

配置openssl的path环境变量,

cmd进入changgou.jks文件所在目录执行如下命令:

keytool -list -rfc --keystore changgou.jks | openssl x509 -inform pem -pubkey


下面段内容是公钥

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAm
t47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnh
cP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEm
oLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/
iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZS
xtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv
9QIDAQAB
-----END PUBLIC KEY-----

将上边的公钥拷贝到文本public.key文件中,合并为一行,可以将它放到需要实现授权认证的工程中。

3.3 基于私钥生成jwt令牌

3.3.1导入认证服务

  1. 将课件中changgou_user_auth的工程导入到项目中去,如下图:
  2. 启动eureka,再启动认证服务

3.3.2 认证服务中创建测试类

public class CreateJwtTest {/**** 创建令牌测试*/@Testpublic void testCreateToken(){//证书文件路径String key_location="changgou.jks";//秘钥库密码String key_password="changgou";//秘钥密码String keypwd = "changgou";//秘钥别名String alias = "changgou";//访问证书路径ClassPathResource resource = new ClassPathResource(key_location);//创建秘钥工厂KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource,key_password.toCharArray());//读取秘钥对(公钥、私钥)KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypwd.toCharArray());//获取私钥RSAPrivateKey rsaPrivate = (RSAPrivateKey) keyPair.getPrivate();//定义PayloadMap<String, Object> tokenMap = new HashMap<>();tokenMap.put("id", "1");tokenMap.put("name", "itheima");tokenMap.put("roles", "ROLE_VIP,ROLE_USER");//生成Jwt令牌Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(rsaPrivate));//取出令牌String encoded = jwt.getEncoded();System.out.println(encoded);}
}

3.4 基于公钥解析jwt令牌

上面创建令牌后,我们可以对JWT令牌进行解析,这里解析需要用到公钥,我们可以将之前生成的公钥public.key拷贝出来用字符串变量token存储,然后通过公钥解密。

在changgou-user-oauth创建测试类com.changgou.token.ParseJwtTest实现解析校验令牌数据,代码如下:

public class ParseJwtTest {/**** 校验令牌*/@Testpublic void testParseToken(){//令牌String token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlcyI6IlJPTEVfVklQLFJPTEVfVVNFUiIsIm5hbWUiOiJpdGhlaW1hIiwiaWQiOiIxIn0.IR9Qu9ZqYZ2gU2qgAziyT38UhEeL4Oi69ko-dzC_P9-Vjz40hwZDqxl8wZ-W2WAw1eWGIHV1EYDjg0-eilogJZ5UikyWw1bewXCpvlM-ZRtYQQqHFTlfDiVcFetyTayaskwa-x_BVS4pTWAskiaIKbKR4KcME2E5o1rEek-3YPkqAiZ6WP1UOmpaCJDaaFSdninqG0gzSCuGvLuG40x0Ngpfk7mPOecsIi5cbJElpdYUsCr9oXc53ROyfvYpHjzV7c2D5eIZu3leUPXRvvVAPJFEcSBiisxUSEeiGpmuQhaFZd1g-yJ1WQrixFvehMeLX2XU6W1nlL5ARTpQf_Jjiw";//公钥String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvFsEiaLvij9C1Mz+oyAmt47whAaRkRu/8kePM+X8760UGU0RMwGti6Z9y3LQ0RvK6I0brXmbGB/RsN38PVnhcP8ZfxGUH26kX0RK+tlrxcrG+HkPYOH4XPAL8Q1lu1n9x3tLcIPxq8ZZtuIyKYEmoLKyMsvTviG5flTpDprT25unWgE4md1kthRWXOnfWHATVY7Y/r4obiOL1mS5bEa/iNKotQNnvIAKtjBM4RlIDWMa6dmz+lHtLtqDD2LF1qwoiSIHI75LQZ/CNYaHCfZSxtOydpNKq8eb1/PGiLNolD4La2zf0/1dlcr5mkesV570NxRmU1tFm8Zd3MZlZmyv9QIDAQAB-----END PUBLIC KEY-----";//校验JwtJwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));//获取Jwt原始内容String claims = jwt.getClaims();System.out.println(claims);//jwt令牌String encoded = jwt.getEncoded();System.out.println(encoded);}
}

4 Oauth2.0入门

4.1 准备工作

  1. 搭建认证服务器之前,先在用户系统表结构中增加如下表结构:
CREATE TABLE `oauth_client_details` (`client_id` varchar(48) NOT NULL COMMENT '客户端ID,主要用于标识对应的应用',`resource_ids` varchar(256) DEFAULT NULL,`client_secret` varchar(256) DEFAULT NULL COMMENT '客户端秘钥,BCryptPasswordEncoder加密',`scope` varchar(256) DEFAULT NULL COMMENT '对应的范围',`authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证模式',`web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '认证后重定向地址',`authorities` varchar(256) DEFAULT NULL,`access_token_validity` int(11) DEFAULT NULL COMMENT '令牌有效期',`refresh_token_validity` int(11) DEFAULT NULL COMMENT '令牌刷新周期',`additional_information` varchar(4096) DEFAULT NULL,`autoapprove` varchar(256) DEFAULT NULL,PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

导入1条初始化数据,其中加密字符明文为changgou:

INSERT INTO `oauth_client_details` VALUES ('changgou', null, '$2a$10$Yvkp3xzDcri6MAsPIqnzzeGBHez1QZR3A079XDdmNU4R725KrkXi2', 'app', 'authorization_code,password,refresh_token,client_credentials', 'http://localhost', null, '43200', '43200', null, null);

4.2 Oauth2授权模式介绍

Oauth2有以下授权模式:

1.授权码模式(Authorization Code)
2.隐式授权模式(Implicit)
3.密码模式(Resource Owner Password Credentials)
4.客户端模式(Client Credentials)

其中授权码模式和密码模式应用较多,本小节介绍授权码模式。

4.2.1 授权码模式

4.2.1.1 授权码授权流程

上边例举的黑马程序员网站使用QQ认证的过程就是授权码模式,流程如下:

1、客户端请求第三方授权

2、用户同意给客户端授权

3、客户端获取到授权码,请求认证服务器申请 令牌

4、认证服务器向客户端响应令牌

5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权

6、资源服务器返回受保护资源

4.2.1.2 申请授权码

请求认证服务获取授权码:

Get请求:
http://localhost:9200/oauth/authorize?client_id=changgou&response_type=code&scop=app&redirect_uri=http://localhost

参数列表如下:

client_id:客户端id,和授权配置类中设置的客户端id一致。
response_type:授权码模式固定为code
scop:客户端范围,和授权配置类中设置的scop一致。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)

首先跳转到登录页面:

输入账号和密码,点击Login。 Spring Security接收到请求会调用UserDetailsService接口的loadUserByUsername方法查询用户正确的密码。 当前导入的基础工程中客户端ID为changgou,秘钥也为changgou即可认证通过。

接下来进入授权页面:

点击Authorize,接下来返回授权码: 认证服务携带授权码跳转redirect_uri,code=k45iLY就是返回的授权码, 每一个授权码只能使用一次

4.2.1.3 申请令牌

拿到授权码后,申请令牌。

Post请求:
http://localhost:9200/oauth/token

参数如下:

grant_type:授权类型,填写authorization_code,表示授权码模式
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

此链接需要使用 http Basic认证。

什么是http Basic认证?

​ http协议定义的一种认证方式,将客户端id和客户端密码按照“客户端ID:客户端密码”的格式拼接,并用base64编 码,放在header中请求服务端,一个例子:

Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=WGNXZWJBcHA6WGNXZWJBcHA= 是用户名:密码的base64编码。 认证失败服务端返回 401 Unauthorized。

以上测试使用postman完成:

http basic认证:


客户端Id和客户端密码会匹配数据库oauth_client_details表中的客户端id及客户端密码。

点击发送: 申请令牌成功

返回信如下:

access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。
jti:当前token的唯一标识
4.2.1.4 令牌校验

Spring Security Oauth2提供校验令牌的端点,如下:

Get: http://localhost:9200/oauth/check_token?token= [access_token]

参数:

token:令牌

使用postman测试如下:

如果令牌校验失败,会出现如下结果:

如果令牌过期了,会如下如下结果:

4.2.1.5 刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码 也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

测试如下: Post:http://localhost:9200/oauth/token

参数:

grant_type: 固定为 refresh_token

refresh_token:刷新令牌(注意不是access_token,而是refresh_token)

4.2.2 密码模式

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接 通过用户名和密码即可申请令牌。

4.2.2.1 申请令牌

测试如下:

Post请求:
http://localhost:9200/oauth/token携带参数:
grant_type:密码模式授权填写password
username:账号
password:密码

并且此链接需要使用 http Basic认证。


4.3 资源服务授权

资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,如下图:

上图的业务流程如下:

1、客户端请求认证服务申请令牌
2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。
3、客户端携带令牌访问资源服务客户端在Http header 中添加: Authorization:Bearer令牌。
4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。
5、令牌有效,资源服务向客户端响应资源信息

4.3.1 用户服务对接Oauth2

基本上所有微服务都是资源服务,这里我们在课程管理服务上配置授权控制,当配置了授权控制后如要访问课程信 息则必须提供令牌。

1、配置公钥 ,将 changggou_user_auth 项目中public.key复制到changgou_service_user中

2、添加依赖

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

3、配置每个系统的Http请求路径安全控制策略以及读取公钥信息识别令牌,如下:

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公钥private static final String PUBLIC_KEY = "public.key";/**** 定义JwtTokenStore* @param jwtAccessTokenConverter* @return*/@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}/**** 定义JJwtAccessTokenConverter* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setVerifierKey(getPubKey());return converter;}/*** 获取非对称加密公钥 Key* @return 公钥 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY);try {InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader);return br.lines().collect(Collectors.joining("\n"));} catch (IOException ioe) {return null;}}/**** Http安全配置,对每个到达系统的http请求链接进行校验* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {//所有请求必须认证通过http.authorizeRequests()//下边的路径放行.antMatchers("/user/add"). //配置地址放行permitAll().anyRequest().authenticated();    //其他地址需要认证授权}
}

4.3.2 资源服务授权测试

不携带令牌访问http://localhost:9005/user

由于该地址受访问限制,需要授权,所以出现如下错误:

{"error": "unauthorized","error_description": "Full authentication is required to access this resource"
}

携带令牌访问http://localhost:9005/user

在http header中添加 Authorization: Bearer 令牌

当输入错误的令牌也无法正常访问资源。

5 认证开发

5.1 需求分析

功能流程图如下:

执行流程:

1、用户登录,请求认证服务
2、认证服务认证通过,生成jwt令牌,将jwt令牌及相关信息写入Redis,并且将身份令牌写入cookie
3、用户访问资源页面,带着cookie到网关
4、网关从cookie获取token,并查询Redis校验token,如果token不存在则拒绝访问,否则放行
5、用户退出,请求认证服务,清除redis中的token,并且删除cookie中的token

使用redis存储用户的身份令牌有以下作用:

1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带token也是无效的。
2、由于jwt令牌过长,不宜存储在cookie中,所以将jwt令牌存储在redis,由客户端请求服务端获取并在客户端存储。

5.2 Redis配置

将认证服务changgou_user_auth中的application.yml配置文件中的Redis配置改成自己对应的端口和密码。

5.3 认证服务

5.3.1 认证需求分析

认证服务需要实现的功能如下:

1、登录接口

前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌存储到redis。 将令牌写入cookie。

2、退出接口 校验当前用户的身份为合法并且为已登录状态。 将令牌从redis删除。 删除cookie中的令牌。

5.3.2 授权参数配置

修改changgou_user_auth中application.yml配置文件,修改对应的授权配置

auth:ttl: 1200  #token存储到redis的过期时间clientId: changgou    #客户端IDclientSecret: changgou    #客户端秘钥cookieDomain: localhost   #Cookie保存对应的域名cookieMaxAge: -1          #Cookie过期时间,-1表示浏览器关闭则销毁

5.3.3 申请令牌测试

为了不破坏Spring Security的代码,我们在Service方法中通过RestTemplate请求Spring Security所暴露的申请令 牌接口来申请令牌,下边是测试代码:

@SpringBootTest
@RunWith(SpringRunner.class)
public class TokenTest {@Autowiredprivate LoadBalancerClient loadBalancerClient;@Autowiredprivate RestTemplate restTemplate;/***** 发送Http请求创建令牌*/@Testpublic void testCreateToken() throws InterruptedException {//采用客户端负载均衡,从eureka获取认证服务的ip 和端口ServiceInstance serviceInstance = loadBalancerClient.choose("USER-AUTH");URI uri = serviceInstance.getUri();//申请令牌地址String authUrl = uri + "/oauth/token";//1、header信息,包括了http basic认证信息MultiValueMap<String, String> headers = new LinkedMultiValueMap<String, String>();//进行Base64编码,并将编码后的认证数据放到头文件中String httpbasic = httpbasic("changgou", "changgou");headers.add("Authorization", httpbasic);//2、指定认证类型、账号、密码MultiValueMap<String, String> body = new LinkedMultiValueMap<String, String>();body.add("grant_type","password");body.add("username","itheima");body.add("password","123456");HttpEntity<MultiValueMap<String, String>> multiValueMapHttpEntity = new HttpEntity<MultiValueMap<String, String>>(body, headers);//指定 restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {//当响应的值为400或401时候也要正常响应,不要抛出异常if (response.getRawStatusCode() != 400 && response.getRawStatusCode() != 401) {super.handleError(response);}}});//远程调用申请令牌ResponseEntity<Map> exchange = restTemplate.exchange(authUrl, HttpMethod.POST, multiValueMapHttpEntity, Map.class);Map result = exchange.getBody();System.out.println(result);}/**** base64编码* @param clientId* @param clientSecret* @return*/private String httpbasic(String clientId,String clientSecret){//将客户端id和客户端密码拼接,按“客户端id:客户端密码”String string = clientId+":"+clientSecret;//进行base64编码byte[] encode = Base64Utils.encode(string.getBytes());return "Basic "+new String(encode);}
}

5.3.4 业务层

AuthService接口:

public interface AuthService {AuthToken login(String username, String password, String clientId, String clientSecret);
}

AuthServiceImpl实现类:

基于刚才写的测试实现申请令牌的service方法如下:

@Service
public class AuthServiceImpl implements AuthService {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate LoadBalancerClient loadBalancerClient;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${auth.ttl}")private long ttl;/*** 申请令牌* @param username* @param password* @param clientId* @param clientSecret* @return*/@Overridepublic AuthToken applyToken(String username, String password, String clientId, String clientSecret) {ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");URI uri = serviceInstance.getUri();String url = uri+"/oauth/token";MultiValueMap<String, String> body = new LinkedMultiValueMap<>();body.add("grant_type","password");body.add("username",username);body.add("password",password);MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body,headers);restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){@Overridepublic void handleError(ClientHttpResponse response) throws IOException {if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){super.handleError(response);}}});ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);Map map = responseEntity.getBody();if (map==null || map.get("access_token")==null || map.get("refresh_token")==null || map.get("jti")==null){throw new RuntimeException("申请令牌失败");}AuthToken authToken = new AuthToken();authToken.setAccessToken((String) map.get("access_token"));authToken.setRefreshToken((String) map.get("refresh_token"));authToken.setJti((String) map.get("jti"));stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);return authToken;}private String getHttpBasic(String clientId, String clientSecret) {String value = clientId+":"+clientSecret;byte[] encode = Base64Utils.encode(value.getBytes());return "Basic "+new String(encode);}
}

5.3.5 控制层

AuthController编写用户登录授权方法,代码如下:

@RestController
@RequestMapping("/oauth")
public class AuthController {@Autowiredprivate AuthService authService;@Value("${auth.clientId}")private String clientId;@Value("${auth.clientSecret}")private String clientSecret;@Value("${auth.cookieDomain}")private String cookieDomain;@Value("${auth.cookieMaxAge}")private int cookieMaxAge;@PostMapping("/login")public Result login(String username,String password){if (StringUtils.isEmpty(username)){throw new RuntimeException("用户名不存在");}if (StringUtils.isEmpty(password)){throw new RuntimeException("密码不存在");}AuthToken authToken = authService.applyToken(username,password,clientId,clientSecret);this.saveJtiToCookie(authToken.getJti());return new Result(true, StatusCode.OK,"登录成功");}private void saveJtiToCookie(String jti) {HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);}
}

5.3.6 登录请求放行

修改认证服务WebSecurityConfig类中configure(),添加放行路径

5.3.7 测试认证接口

使用postman测试:

1)Post请求:http://localhost:9200/oauth/login

5.3.8 动态获取用户信息

​ 当前在认证服务中,用户密码是写死在用户认证类中。所以用户登录时,无论帐号输入什么,只要密码是itheima都可以访问。 因此需要动态获取用户帐号与密码.

5.3.8.1 定义被访问接口

用户微服务对外暴露根据用户名获取用户信息接口

@GetMapping("/load/{username}")
public User findUserInfo(@PathVariable("username") String username){return userService.findById(username);
}
5.3.8.2 放行该接口,修改ResourceServerConfig类

5.3.8.3定义feign接口

changgou_user_server_api新增feign接口

@FeignClient(name="user")
public interface UserFeign {@GetMapping("/user/load/{username}")public User findUserInfo(@PathVariable("username") String username);
}
5.3.8.4 认证服务添加依赖
<dependency><groupId>com.changgou</groupId><artifactId>changgou_service_user_api</artifactId><version>1.0-SNAPSHOT</version>
</dependency>
5.3.8.5 修改认证服务启动类
@EnableFeignClients(basePackages = "com.changgou.user.feign")
5.3.8.6 修改用户认证类


测试: 重新启动服务并申请令牌

6 认证服务对接网关

6.1 新建网关工程changgou_gateway_web

  1. changgou_gateway添加依赖
 <!--网关依赖--><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId><version>2.1.3.RELEASE</version></dependency></dependencies>
  1. 新建工程changgou_gateway_web,并创建启动类
@SpringBootApplication
@EnableDiscoveryClient
public class WebGateWayApplication {public static void main(String[] args) {SpringApplication.run(WebGateWayApplication.class,args);}
}
  1. 创建application.yml
spring:application:name: gateway-webcloud:gateway:globalcors:cors-configurations:'[/**]': # 匹配所有请求allowedOrigins: "*" #跨域处理 允许所有的域allowedMethods: # 支持的方法- GET- POST- PUT- DELETEroutes:- id: changgou_goods_routeuri: lb://goodspredicates:- Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**filters:#- PrefixPath=/brand- StripPrefix=1#用户微服务- id: changgou_user_routeuri: lb://userpredicates:- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**filters:- StripPrefix=1#认证微服务- id: changgou_oauth_useruri: lb://user-authpredicates:- Path=/api/oauth/**filters:- StripPrefix=1redis:host: 192.168.200.128
server:port: 8001
eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: true
management:endpoint:gateway:enabled: trueweb:exposure:include: true

6.2 网关全局过滤器


新建过滤器类AuthorizeFilter,对请求进行过滤

业务逻辑:

1)判断当前请求是否为登录请求,是的话,则放行

  1. 判断cookie中是否存在信息, 没有的话,拒绝访问

3)判断redis中令牌是否存在,没有的话,拒绝访问

@Component
public class AuthFilter implements GlobalFilter, Ordered {public static final String Authorization = "Authorization";@Autowiredprivate AuthService authService;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//获取当前请求对象ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();String path = request.getURI().getPath();if ("/api/oauth/login".equals(path)){//放行return chain.filter(exchange);}//判断cookie上是否存在jtiString jti = authService.getJtiFromCookie(request);if (StringUtils.isEmpty(jti)){//拒绝访问,请求跳转response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//判断redis中token是否存在String redisToken = authService.getTokenFromRedis(jti);if (StringUtils.isEmpty(redisToken)){//拒绝访问,请求跳转response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//校验通过 , 请求头增强,放行request.mutate().header(Authorization,"Bearer "+redisToken);return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}
}

新建业务逻辑类AuthService

@Service
public class AuthService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;/*** 判断cookie中jti是否存在* @param request* @return*/public String getJtiFromCookie(ServerHttpRequest request) {HttpCookie cookie = request.getCookies().getFirst("uid");if (cookie!=null){return cookie.getValue();}return null;}/*** 判断redis中令牌是否过期* @param jti* @return*/public String getTokenFromRedis(String jti) {String token = stringRedisTemplate.boundValueOps(jti).get();return token;}
}

测试:

清除postman中的cookie数据,在未登录的情况下,并访问: http://localhost:8001/api/user 。会返回401异常信息

访问:http://localhost:8001/api/oauth/login,并重新测试。可以发现测试通过,拿到返回结果数据

7 自定义登录页面

7.1 认证服务添加依赖

<!--thymeleaf-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

7.2 资源\成品页面\登录页面

7.3 把页面放到下面的项目中

7.4 静态资源放行

修改WebSecurityConfig类

web.ignoring().antMatchers("/oauth/login","/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");

添加开启表单登录

7.5 LoginRedirectController

@Controller
@RequestMapping("/oauth")
public class LoginRedirectController {@RequestMapping("/toLogin")public String toLogin(){return "login";}
}

7.6 修改登录页面实现用户登录

7.7 定义login方法

<script th:inline="javascript">var app = new Vue({el:"#app",data:{username:"",password:"",msg:""},methods:{login:function(){app.msg="正在登录";axios.post("/api/oauth/login?username="+app.username+"&password="+app.password).then(function (response) {if (response.data.flag){app.msg="登录成功";}else{app.msg="登录失败";}})}}})
</script>

定义路径过滤

public class URLFilter {public static String filterPath = "/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";public static boolean hasAuthorize(String url){String[] split = filterPath.replace("**", "").split(",");for (String value : split) {if (url.startsWith(value)){return true;}}return false;}
}

7.8 测试

访问:http://localhost:9200/oauth/toLogin

8权限控制

用户每次访问微服务的时候,先去oauth2.0服务登录,登录后再访问微服务网关,微服务网关将请求转发给其他微服务处理。

​ 由于我们项目使用了微服务,任何用户都有可能使用任意微服务,此时我们需要控制相关权限,例如:普通用户角色不能使用用户的删除操作,只有管理员才可以使用,那么这个时候就需要使用到SpringSecurity的权限控制功能了。

注解方式
直接在上面配置一段@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
用于开启@PreAuthorize的支持,代码如下:

package com.changgou.user.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {//公钥private static final String PUBLIC_KEY = "public.key";/**** 定义JwtTokenStore* @param jwtAccessTokenConverter* @return*/@Beanpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}/**** 定义JJwtAccessTokenConverter* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setVerifierKey(getPubKey());return converter;}/*** 获取非对称加密公钥 Key* @return 公钥 Key*/private String getPubKey() {Resource resource = new ClassPathResource(PUBLIC_KEY);try {InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());BufferedReader br = new BufferedReader(inputStreamReader);return br.lines().collect(Collectors.joining("\n"));} catch (IOException ioe) {return null;}}/**** Http安全配置,对每个到达系统的http请求链接进行校验* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {//所有请求必须认证通过http.authorizeRequests()//下边的路径放行.antMatchers("/user/add","/user/load/**"). //配置地址放行permitAll().anyRequest().authenticated();    //其他地址需要认证授权}
}

然后就可以在controller类上加入@PreAuthorize
如果希望一个方法能被多个角色访问,配置:@PreAuthorize("hasAnyAuthority('admin','user')")

如果希望一个类都能被多个角色访问,在类上配置:@PreAuthorize("hasAnyAuthority('admin','user')")

oauth全部代码

pox.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>changgou_parent</artifactId><groupId>com.changgou</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>changgou-user-oauth</artifactId><description>OAuth2.0认证环境搭建</description><dependencies><!--查询数据库数据--><dependency><groupId>com.changgou</groupId><artifactId>changgou_common_db</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-data</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>com.changgou</groupId><artifactId>changgou_service_user_api</artifactId><version>1.0-SNAPSHOT</version></dependency><!--thymeleaf--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies></project>

yml

server:port: 9200
spring:application:name: user-authredis:host: 192.168.200.128port: 6379password:jedis:pool:max-active: 8max-idle: 8min-idle: 0datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://192.168.200.128:3306/changgou_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTCusername: rootpassword: rootmain:allow-bean-definition-overriding: true
eureka:instance:prefer-ip-address: trueclient:service-url:defaultZone: http://127.0.0.1:6868/eureka
auth:ttl: 3600  #token存储到redis的过期时间clientId: changgouclientSecret: changgoucookieDomain: localhostcookieMaxAge: -1
encrypt:key-store:location: classpath:/changgou.jkssecret: changgoualias: changgoupassword: changgou

util中的
AuthToken

package com.changgou.oauth.util;import java.io.Serializable;public class AuthToken implements Serializable{//令牌信息 jwtString accessToken;//刷新token(refresh_token)String refreshToken;//jwt短令牌String jti;public String getAccessToken() {return accessToken;}public void setAccessToken(String accessToken) {this.accessToken = accessToken;}public String getRefreshToken() {return refreshToken;}public void setRefreshToken(String refreshToken) {this.refreshToken = refreshToken;}public String getJti() {return jti;}public void setJti(String jti) {this.jti = jti;}
}

CookieUtil

package com.changgou.oauth.util;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;public class CookieUtil {/*** 设置cookie** @param response* @param name     cookie名字* @param value    cookie值* @param maxAge   cookie生命周期 以秒为单位*/public static void addCookie(HttpServletResponse response, String domain, String path, String name,String value, int maxAge, boolean httpOnly) {Cookie cookie = new Cookie(name, value);cookie.setDomain(domain);cookie.setPath(path);cookie.setMaxAge(maxAge);cookie.setHttpOnly(httpOnly);response.addCookie(cookie);}/*** 根据cookie名称读取cookie* @param request* @return map<cookieName,cookieValue>*/public static Map<String,String> readCookie(HttpServletRequest request, String ... cookieNames) {Map<String,String> cookieMap = new HashMap<String,String>();Cookie[] cookies = request.getCookies();if (cookies != null) {for (Cookie cookie : cookies) {String cookieName = cookie.getName();String cookieValue = cookie.getValue();for(int i=0;i<cookieNames.length;i++){if(cookieNames[i].equals(cookieName)){cookieMap.put(cookieName,cookieValue);}}}}return cookieMap;}
}

UserJwt

package com.changgou.oauth.util;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;import java.util.Collection;public class UserJwt extends User {private String id;    //用户IDprivate String name;  //用户名字public UserJwt(String username, String password, Collection<? extends GrantedAuthority> authorities) {super(username, password, authorities);}public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}

启动类

package com.changgou;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
import tk.mybatis.spring.annotation.MapperScan;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.changgou.auth.dao")
@EnableFeignClients(basePackages = {"com.changgou.user.feign"})
public class OAuthApplication {public static void main(String[] args) {SpringApplication.run(OAuthApplication.class,args);}@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}
}

重点的config
AuthorizationServerConfig

package com.changgou.oauth.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {//数据源,用于从数据库获取数据进行认证操作,测试可以从内存中获取@Autowiredprivate DataSource dataSource;//jwt令牌转换器@Autowiredprivate JwtAccessTokenConverter jwtAccessTokenConverter;//SpringSecurity 用户自定义授权认证类@AutowiredUserDetailsService userDetailsService;//授权认证管理器@AutowiredAuthenticationManager authenticationManager;//令牌持久化存储接口@AutowiredTokenStore tokenStore;@Autowiredprivate CustomUserAuthenticationConverter customUserAuthenticationConverter;/**** 客户端信息配置* @param clients* @throws Exception*/@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.jdbc(dataSource).clients(clientDetails());}/**** 授权服务器端点配置* @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {endpoints.accessTokenConverter(jwtAccessTokenConverter).authenticationManager(authenticationManager)//认证管理器.tokenStore(tokenStore)                       //令牌存储.userDetailsService(userDetailsService);     //用户信息service}/**** 授权服务器的安全配置* @param oauthServer* @throws Exception*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {oauthServer.allowFormAuthenticationForClients().passwordEncoder(new BCryptPasswordEncoder()).tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");}//读取密钥的配置@Bean("keyProp")public KeyProperties keyProperties(){return new KeyProperties();}@Resource(name = "keyProp")private KeyProperties keyProperties;//客户端配置@Beanpublic ClientDetailsService clientDetails() {return new JdbcClientDetailsService(dataSource);}@Bean@Autowiredpublic TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {return new JwtTokenStore(jwtAccessTokenConverter);}/***** JWT令牌转换器* @param customUserAuthenticationConverter* @return*/@Beanpublic JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore().getLocation(),                          //证书路径 changgou.jkskeyProperties.getKeyStore().getSecret().toCharArray())              //证书秘钥 changgouapp.getKeyPair(keyProperties.getKeyStore().getAlias(),                     //证书别名 changgoukeyProperties.getKeyStore().getPassword().toCharArray());   //证书密码 changgouconverter.setKeyPair(keyPair);//配置自定义的CustomUserAuthenticationConverterDefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);return converter;}
}

CustomUserAuthenticationConverter

package com.changgou.oauth.config;import com.changgou.oauth.util.UserJwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter;
import org.springframework.stereotype.Component;import java.util.LinkedHashMap;
import java.util.Map;@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {@AutowiredUserDetailsService userDetailsService;@Overridepublic Map<String, ?> convertUserAuthentication(Authentication authentication) {LinkedHashMap response = new LinkedHashMap();String name = authentication.getName();response.put("username", name);Object principal = authentication.getPrincipal();UserJwt userJwt = null;if(principal instanceof  UserJwt){userJwt = (UserJwt) principal;}else{//refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwtUserDetails userDetails = userDetailsService.loadUserByUsername(name);userJwt = (UserJwt) userDetails;}response.put("name", userJwt.getName());response.put("id", userJwt.getId());if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));}return response;}}

UserDeatilsServiceImpl

package com.changgou.oauth.config;import com.changgou.oauth.util.UserJwt;
import com.changgou.user.feign.UserFeign;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;/****** 自定义授权认证类*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@AutowiredClientDetailsService clientDetailsService;@Autowiredprivate UserFeign userFeign;/***** 自定义授权认证* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//取出身份,如果身份为空说明没有认证Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//没有认证统一采用httpbasic认证,httpbasic中存储了client_id和client_secret,开始认证client_id和client_secretif(authentication==null){ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);if(clientDetails!=null){//秘钥String clientSecret = clientDetails.getClientSecret();//静态方式//return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));//数据库查找方式return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));}}if (StringUtils.isEmpty(username)) {return null;}//根据用户名查询用户信息//String pwd = new BCryptPasswordEncoder().encode("itheima");com.changgou.user.pojo.User user = userFeign.findUserInfo(username);//创建User对象String permissions = "goods_list,seckill_list";UserJwt userDetails = new UserJwt(username,user.getPassword(),AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));return userDetails;}
}

WebSecurityConfig

package com.changgou.oauth.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {/**** 忽略安全拦截的URL* @param web* @throws Exception*/@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/oauth/login","/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");}/**** 创建授权管理认证对象* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {AuthenticationManager manager = super.authenticationManagerBean();return manager;}/**** 采用BCryptPasswordEncoder对密码进行编码* @return*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/****** @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().httpBasic()        //启用Http基本身份验证.and().formLogin()       //启用表单身份验证.and().authorizeRequests()    //限制基于Request请求访问.anyRequest().authenticated();       //其他请求都需要经过验证//开启表单登录http.formLogin().loginPage("/oauth/toLogin")//设置访问登录页面的路径.loginProcessingUrl("/oauth/login");//设置执行登录操作的路径}
}

AuthService

package com.changgou.oauth.service;import com.changgou.oauth.util.AuthToken;public interface AuthService {AuthToken login(String username,String password,String clientId,String clientSecret);
}

AuthServiceImpl

package com.changgou.oauth.service.impl;import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.util.Base64Utils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.concurrent.TimeUnit;@Service
public class AuthServiceImpl implements AuthService {@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate LoadBalancerClient loadBalancerClient;@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Value("${auth.ttl}")private long ttl;@Overridepublic AuthToken login(String username, String password, String clientId, String clientSecret) {//1.申请令牌ServiceInstance serviceInstance = loadBalancerClient.choose("user-auth");URI uri = serviceInstance.getUri();String url=uri+"/oauth/token";MultiValueMap<String, String> body = new LinkedMultiValueMap<>();body.add("grant_type","password");body.add("username",username);body.add("password",password);MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){@Overridepublic void handleError(ClientHttpResponse response) throws IOException {if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){super.handleError(response);}}});ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);Map map = responseEntity.getBody();if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){//申请令牌失败throw new RuntimeException("申请令牌失败");}//2.封装结果数据AuthToken authToken = new AuthToken();authToken.setAccessToken((String) map.get("access_token"));authToken.setRefreshToken((String) map.get("refresh_token"));authToken.setJti((String)map.get("jti"));//3.将jti作为redis中的key,将jwt作为redis中的value进行数据的存放stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.SECONDS);return authToken;}private String getHttpBasic(String clientId, String clientSecret) {String value = clientId+":"+clientSecret;byte[] encode = Base64Utils.encode(value.getBytes());return "Basic "+new String(encode);}
}

AuthController

package com.changgou.oauth.controller;import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.changgou.oauth.service.AuthService;
import com.changgou.oauth.util.AuthToken;
import com.changgou.oauth.util.CookieUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletResponse;@Controller
@RequestMapping("/oauth")
public class AuthController {@Autowiredprivate AuthService authService;@Value("${auth.clientId}")private String clientId;@Value("${auth.clientSecret}")private String clientSecret;@Value("${auth.cookieDomain}")private String cookieDomain;@Value("${auth.cookieMaxAge}")private int cookieMaxAge;@RequestMapping("/toLogin")public String toLogin(){return "login";}@RequestMapping("/login")@ResponseBodypublic Result login(String username, String password, HttpServletResponse response){//校验参数if (StringUtils.isEmpty(username)){throw new RuntimeException("请输入用户名");}if (StringUtils.isEmpty(password)){throw new RuntimeException("请输入密码");}//申请令牌 authtokenAuthToken authToken = authService.login(username, password, clientId, clientSecret);//将jti的值存入cookie中this.saveJtiToCookie(authToken.getJti(),response);//返回结果return new Result(true, StatusCode.OK,"登录成功",authToken.getJti());}//将令牌的断标识jti存入到cookie中private void saveJtiToCookie(String jti, HttpServletResponse response) {CookieUtil.addCookie(response,cookieDomain,"/","uid",jti,cookieMaxAge,false);}
}

gateway_web

<!--网关依赖--><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId><version>2.1.3.RELEASE</version></dependency></dependencies>

yml

spring:application:name: gateway-webcloud:gateway:globalcors:cors-configurations:'[/**]': # 匹配所有请求allowedOrigins: "*" #跨域处理 允许所有的域allowedMethods: # 支持的方法- GET- POST- PUT- DELETEroutes:- id: changgou_goods_routeuri: lb://goodspredicates:- Path=/api/album/**,/api/brand/**,/api/cache/**,/api/categoryBrand/**,/api/category/**,/api/para/**,/api/pref/**,/api/sku/**,/api/spec/**,/api/spu/**,/api/stockBack/**,/api/template/**filters:#- PrefixPath=/brand- StripPrefix=1#用户微服务- id: changgou_user_routeuri: lb://userpredicates:- Path=/api/user/**,/api/address/**,/api/areas/**,/api/cities/**,/api/provinces/**filters:- StripPrefix=1#认证微服务- id: changgou_oauth_useruri: lb://user-authpredicates:- Path=/api/oauth/**filters:- StripPrefix=1redis:host: 192.168.200.128
server:port: 8001
eureka:client:service-url:defaultZone: http://127.0.0.1:6868/eurekainstance:prefer-ip-address: true
management:endpoint:gateway:enabled: trueweb:exposure:include: true

filter中的
AuthFilter

package com.changgou.web.gateway.filter;import com.changgou.web.gateway.service.AuthService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Component
public class AuthFilter implements GlobalFilter, Ordered {@Autowiredprivate AuthService authService;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();//1.判断当前请求路径是否为登录请求,如果是,则直接放行String path = request.getURI().getPath();if ("/api/oauth/login".equals(path) || !UrlFilter.hasAuthorize(path) ){//直接放行return chain.filter(exchange);}//2.从cookie中获取jti的值,如果该值不存在,拒绝本次访问String jti = authService.getJtiFromCookie(request);if (StringUtils.isEmpty(jti)){//拒绝访问response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//3.从redis中获取jwt的值,如果该值不存在,拒绝本次访问String jwt = authService.getJwtFromRedis(jti);if (StringUtils.isEmpty(jwt)){//拒绝访问response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//4.对当前的请求对象进行增强,让它会携带令牌的信息request.mutate().header("Authorization","Bearer "+jwt);return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}
}

UrlFilter

package com.changgou.web.gateway.filter;public class UrlFilter {//所有需要传递令牌的地址public static String filterPath="/api/wseckillorder,/api/seckill,/api/wxpay,/api/wxpay/**,/api/worder/**,/api/user/**,/api/address/**,/api/wcart/**,/api/cart/**,/api/categoryReport/**,/api/orderConfig/**,/api/order/**,/api/orderItem/**,/api/orderLog/**,/api/preferential/**,/api/returnCause/**,/api/returnOrder/**,/api/returnOrderItem/**";public static boolean hasAuthorize(String url){String[] split = filterPath.replace("**", "").split(",");for (String value : split) {if (url.startsWith(value)){return true; //代表当前的访问地址是需要传递令牌的}}return false; //代表当前的访问地址是不需要传递令牌的}
}

AuthService

package com.changgou.web.gateway.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;@Service
public class AuthService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;//从cookie中获取jti的值public String getJtiFromCookie(ServerHttpRequest request) {HttpCookie httpCookie = request.getCookies().getFirst("uid");if (httpCookie != null){String jti = httpCookie.getValue();return jti;}return null;}//从redis中获取jwtpublic String getJwtFromRedis(String jti) {String jwt = stringRedisTemplate.boundValueOps(jti).get();return jwt;}
}

登录模块 用户认证 SpringSecurity +Oauth2+Jwt相关推荐

  1. springboot 、thymeleaf、pagehelper 、springsecurity实现 登录,用户认证,分页的前端使用妹子UI

    springboot  .thymeleaf.pagehelper .springsecurity 实现 登录,用户认证,分页的前端使用妹子UIhttp://tpl.amazeui.org/. 项目下 ...

  2. SpringBoot+SpringSecurity之多模块用户认证授权同步

    在之前的文章里介绍了SpringBoot和SpringSecurity如何继承.之后我们需要考虑另外一个问题:当前微服务化也已经是大型网站的趋势,当我们的项目采用微服务化架构时,往往会出现如下情况: ...

  3. 整合SpringSecurity OAuth2 JWT (附原因)一步步来如此简单

    梳理整合 SpringCloud 和 SpringSecurity OAuth2 的搭建流程,网上看了一些,感觉都很差强人意,所以决定梳理下,不多说了开鲁吧. 首先要搭建微服务基础加包版本,基于阿里系 ...

  4. springsecurity oauth2使用jwt实现单点登录

    Jwt方式已经分享在文章结尾处的百度网盘链接中,redis方式可以看我以前发表的文章. 文章目录 前言 一.springsecurity oauth2 + redis方式的缺点 二.oauth2认证的 ...

  5. springboot整合jwt_Spring Boot整合JWT实现用户认证(附源码)

    作者:LTLAYXhttps://dwz.cn/yv1Do6e3 推荐阅读 1. Java 性能优化:教你提高代码运行的效率 2. 基于token的多平台身份认证架构设计 3. select coun ...

  6. Spring Boot整合JWT实现用户认证

    JWT实现用户认证 在介绍完JWT之后我们使用springboot整合JWT实现用户认证. 前后端分离使用JWT做用户认证(概述) JWT实现认证的原理 ​服务器在生成一个JWT之后会将这个JWT会以 ...

  7. Spring Boot整合JWT实现用户认证(附源码)

    点击上方"程序IT圈",选择"置顶公众号" 每天早上8点50分进来看看,就是最大的支持 来源:https://dwz.cn/yv1Do6e3 什么是JWT JW ...

  8. Spring Cloud Alibaba 统一门户:基于网关的统一用户认证方案

    本讲咱们涉及以下三方面内容: 传统的用户认证方案: JWT 与 JJWT: 基于网关的统一用户认证. 传统的用户认证方案 我们直奔主题,什么是用户认证呢?对于大多数与用户相关的操作,软件系统首先要确认 ...

  9. 14 医疗挂号系统_【阿里云OSS、用户认证与就诊人】

    阿里云oss 用户认证需要上传证件图片.首页轮播也需要上传图片,因此我们要做文件服务,阿里云oss是一个很好的分布式文件服务系统,所以我们只需要集成阿里云oss即可 1.开通"对象存储OSS ...

  10. Spring Cloud微服务网关Zuul过滤链和整合OAuth2+JWT入门实战

    一.Spring Cloud Zuul 过滤链 1.1 工作原理 Zuul的核心逻辑是由一系列的Filter来实现的,他们能够在进行HTTP请求或者相应的时候执行相关操作.Zuul Filter的主要 ...

最新文章

  1. mysql 5.7 Stage Tracking DDL进度跟踪
  2. spring的xml配置文件出现故障
  3. 小红书增长负责人:精确化营销如何玩
  4. the data structure i like
  5. 【ARM】ARM汇编程序设计(一)
  6. 10、mysql数据表中数据的查询(2)
  7. kitti pkl可视化_KITTI 3D Lidar 数据可视化
  8. VMware8 8.0.1 安装Lion正式版 原版镜像 直接DMG安装 非整合版 免引导 完美解决iCloud...
  9. 复制release文件到另一台电脑.exe文件无法运行_电脑技巧:电脑版微信双开(或微信多开)?用start指令可以解决...
  10. Linux 常见问题 1000 个详细解答
  11. 项目管理-让什么人做什么事
  12. 安装Kibana报错[warning][admin][elasticsearch] Unable to revive connection: http://localhost:9200/
  13. 在线QuartzCron定时任务表达式在线生成
  14. 问题:虚拟机安装Ubuntu时,显示PXE-MOF:Exiting Intel PXE ROM.Operating Systen not found
  15. 企业信息化建设的意义
  16. ABB伺服驱动器调试(一)
  17. 51单片机间接寻址C语言,51单片机寄存器间接寻址方式与举例
  18. 简述网桥的特点_网桥的特点
  19. 甲乙2个人去买冬瓜,甲买差7元,乙买差9元,合买差1元,冬瓜多少钱?
  20. 微信扫码(扫一扫二维码)下载不了app怎么解决

热门文章

  1. 在计算机领域中 媒体是,在计算机中,媒体是指什么
  2. android使用百度地图SDK 去掉百度Logo的小技巧
  3. MATLAB实现Enigma 密码机
  4. 一个简单帆软报表制作基础步骤
  5. 软件工程笔记四__实体联系图(ER图)
  6. 关于E-R(实体-联系)图
  7. cppcheck 自定义规则_CppCheck介绍与使用
  8. 包含太多_股票开户必备知识,股票交易费包含哪些?
  9. 统计学 参数估计 之 总体均值的估计
  10. 塑胶模具注塑常用哪些材质