【深入理解JWT】从认证授权到网关安全
最近的项目学习中,在进行登陆模块的用户信息验证这一部分又用到了JWT的一些概念和相关知识,特在此写了这篇文章、方便各位笔者理解JWT相关概念
目录
先来理解JWT是什么?
区分有状态认证和无状态认证
有状态认证 VS 无状态认证
JWT令牌的优点
JWT令牌的三个部分
测试生成JWT令牌
实例:携带令牌访问资源服务
网关 — 完善架构
先来理解JWT是什么?
JWT全称为JSON Web Token是一种开放标准(RFC 7519),主要用于在网络应用间安全地传递信息,是一种基于JSON的紧凑,独立的令牌格式,通常用于身份验证和信息交换
它的主要使用场景:
用于身份验证,用于登录和后续请求的验证
举一个现实中的例子,假设我们正在使用淘宝平台,登录流程大致如下:
1.当我们输入了用户名和密码,点击”登录“,网站服务器验证你的凭证
2.验证成功后,服务器生成了一个JWT返回给你的浏览器
3.如果继续浏览商品或下单时,浏览器会在请求中携带这个JWT,网站服务器通过验证JWT确认你的身份
4.确认身份后,就能访问购物车,下单或查看订单详情
在分布式系统或多服务架构中,JWT负责在不同的服务之间传递用户信息
假设有一个电商平台,其中包括两个服务:服务A(用户服务)和服务B(订单服务),用户通过服务A登陆后,需要将用户信息传递给服务B完成订单操作,以便服务B能够根据用户身份处理订单相关的操作
区分有状态认证和无状态认证
有状态认证介绍:
传统的基于session的方式是有状态认证,用户登录成功将用户的身份信息存储在服务端,这样会加大服务端的存储压力,这种方式也不适合在分布式系统中应用
如下图,当用户访问应用服务,每个应用服务都会去服务器查看session信息,如果session中没有该用户则说明用户没有登录,此时就会重新认证,而解决这个问题的方法就是Session复制,Session黏贴
无状态认证:
如果是基于令牌技术在分布式系统中实现认证,则服务端不用存储session,可以将用户身份信息存储在令牌中,用户认证通过后认证服务颁发令牌给用户,用户将令牌存储在客户端,去访问应用服务时携带令牌去访问,服务端从JWT解析出用户信息,这个过程就是无状态认证
有状态认证 VS 无状态认证
有状态流程:
用户登录 —> 服务端创建Session并存储用户信息 —> 返回Session ID给客户端 —> 客户端携带 Session ID访问应用服务 —> 服务端根据Session ID查找用户信息 —> 返回响应
优点:
1.安全性较高:用户的信息存储在服务端,客户端仅持有Session ID,不容易泄露敏感数据
2.易于控制:服务端可以随时使Session失效,比如用户注销,超时
缺点:
1.服务端存储压力大:每个用户的Session信息都需要存储在服务端,用户量较大时对内存和存储资源要求高
2.不适合分布系统:在分布式系统中,Session需要共享,比如通过Redis集群,复杂性增加,新增服务器时 还需要同步Session数据
无状态认证流程:
用户登录 —> 服务端生成Token(令牌)并返回给客户端 —> 客户端携带Token(令牌)访问应用服务 —> 服务端解析Token(令牌)获取用户信息 —>返回响应
优点:
1.无需存储信息:服务端无需存储用户信息,适合分布式系统和微服务架构
2.无需同步:新增服务器时无需同步数据,降低了运维成本
3.实现跨域共享:令牌可以轻松实现跨域资源共享
缺点:
1.令牌长度较大,比Session ID长,可能会增加网络开销
2.Token在过期前始终有效,无法像Session一样主动注销
3.JWT的Payload是Base64编码的,可以被解码,不适合存储敏感信息
分析这两个有无状态认证,总结出以下几点:
有状态认证:更适合传统Web应用,像银行系统这样需要严格会话控制的场景
无状态认证:更适合分布式系统,微服务架构,移动端和前后端分离的应用
JWT令牌的优点
1.JWT基于JSON,易于解析:
JWT的Header和Payload是Base64编码的,解码后可以直接解析为JSON对象,开发者可以轻松地从JWT中提取所需的信息,比如用户ID,角色..
{
"userId": 123,
"username": "john_doe",
"role": "admin"
}
2.可以在令牌中自定义丰富的内容:
JWT的Payload部分可以包含任意自定义的声明(claims),可以在Payload中添加用户角色,权限,过期时间等信息,如果要添加新的信息(如添加用户邮箱,地址等),只需在Payload中添加新的字段,无需修改现有逻辑
{
"userId": 123,
"username": "john_doe",
"role": "admin",
//新增了邮箱 电话等信息
"email": "john@example.com",
"exp": 1698765432
}
3.通过非对称加密算法以及数字签名技术,防止篡改,安全性高
JWT的Signature部分是通过Header和Payload使用指定的算法(如HMAC SHA256或RSA SHA256)生成的
如果攻击者修改了JWT的Header或Payload,签名将不再匹配,验证时会失败
同样的,缺点如下:JWT令牌较长,占的内存存储空间较大
JWT令牌的示例
JWT令牌的三个部分
以上这段JWT令牌代码包含了三部分,用点号(.)分隔
Header:头部包括令牌的类型(既JWT)以及使用的哈希算法,HMAC SHA256或RSA
Base64编码前的JSON
{
"alg": "HS256", //alg:签名算法,这里是HS256
"typ": "JWT" //令牌类型,这里是JWT
}
Base64编码后的Header(以上令牌示例图第一部分)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload:第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,可以存放jwt提供的信息字段,比如:iss(签发者),exp(过期时间戳),sub(面向的用户)等,也可以自定义字段
Base64编码前的Payload
{
"aud": ["res1"], //令牌的目标受众,这里是["rset"]
"user_name": "zhangsan",//用户名
"scope": ["all"], //权限范围
"exp": 1664254672, //令牌的过期时间(Unix时间戳)
"authorities": ["p1"], //用户的权限,这里是["p1"]
"jti": "88912b2d-5d05-4c14-bbc3-fde9977febc6",//令牌的唯一标识符
"client_id": "c1" //客户端ID,这里是c1
}
Base64编码后的Payload(以上令牌示例图第二部分)
eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQyNTQ2NzIsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijg4OTEyYjJk
LTVkMDUtNGMxNC1iYmMzLWZkZTk5NzdmZWJjNiIsImNsaWVudF9pZCI6ImMxIn0
Signature(签名):签名是通过对Header和Payload进行加密生成的,将Header和Payload分别进行Base64Url编码
编码后将Header,Payload和Signature用点号(.)拼接起来,形成完整的JWT
//拼接Header + payload + 对Header和Payload的签名(验证令牌是否被篡改)
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret) //secret:签名所使用的密钥
(以上令牌示例图)最后一部分就是编码后的Signature
wkDBL7roLrvdBG2oGnXeoXq-zZRgE9IVV2nxd-ez_oA
为什么JWT可以防止篡改?
第三部分使用签名算法对Header和Payload的内容进行签名,常见的签名算法是HS256,常见的还有MD5,SHA等...签名算法需要使用密钥进行签名,密钥不对外公开,并且签名是不可逆的,如果第三方更改了内容那么服务器验证签名就会失败,要想保证验证签名正确必须保证内容,密钥与签名前一致
从上图中可以看出认证服务和资源服务使用了相同的密钥,这叫做对称加密,对称加密效率高,如果一旦密钥泄露可以伪造JWT令牌
JWT还可以使用非对称加密,认证服务自己保留私钥,将公钥下发给受信任的客户端,资源服务,公钥和私钥是配对的,成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密,非对称加密更安全一些
测试生成JWT令牌
在认证服务中配置JWT令牌服务,即可实现生成JWT格式的令牌
package com.xuecheng.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import java.util.Arrays;
/**
* @author Administrator
* @version 1.0
**/
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
@Autowired
TokenStore tokenStore;
// @Bean
// public TokenStore tokenStore() {
// //使用内存存储令牌(普通令牌)
// return new InMemoryTokenStore();
// }
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
//使用JwtTokenStore替代默认的InMemoryTokenStore,表示令牌以JWT格式存储
}
//配置JWT的签名密钥和转换逻辑
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
//令牌管理服务 定义令牌服务的核心行为(如令牌生成,刷新,有效期等)
@Bean(name="authorizationServerTokenServicesCustom")
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service=new DefaultTokenServices();
service.setSupportRefreshToken(true);//支持刷新令牌
service.setTokenStore(tokenStore);//令牌存储策略
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
}
重启认证服务
使用httpclient通过密码模式申请令牌
### 密码模式
POST {{auth_host}}/oauth/token?client_id=XcWebApp&client_secret=
XcWebApp&grant_type=password&username=zhangsan&password=123
生成的JWT示例如下:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOi
J6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzE2OTUsImF1dGhvcml0aWVzIj
pbInAxIl0sImp0aSI6ImU5ZDNkMGZkLTI0Y2ItNDRjOC04YzEwLTI1NmIzNGY4ZGZjYyIsImNsaW
VudF9pZCI6ImMxIn0.-9SKI-qUqKhKcs8Gb80Rascx-JxqsNZxxXoPo82d8SM", //生成的JWT令牌
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ
6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiJlOWQzZDBmZC0yNGNiLTQ0YzgtOGMxMC0y
NTZiMzRmOGRmY2MiLCJleHAiOjE2NjQ1ODM2OTUsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6I
mRjNTRjNTRkLTA0YTMtNDIzNS04MmY3LTFkOWZkMmFjM2VmNSIsImNsaWVudF9pZCI6ImMxIn0.Ws
w1Jc-Kd_GFqEugzdfoSsMY6inC8OQsraA21WjWtT8",
"expires_in": 7199,
"scope": "all",
"jti": "e9d3d0fd-24cb-44c8-8c10-256b34f8dfcc"
}
1.access-token部分:这部分是生成的JWT令牌,用于访问资源使用
2.token_type: 这部分的bearer是在REC6750中定义的一种token类型,在携带JWT访问资源时需要在head中加入bearer jwt令牌内容
###校验jwt令牌
POST {{auth_host}}/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJzdHUxIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY2NDM3MTc4MCwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiZjBhM2NkZWItMzk5ZC00OGYwLTg4MDQtZWNhNjM4YWQ4ODU3IiwiY2xpZW50X2lkIjoiYzEifQ.qy46CSCJsH3eXWTHgdcntZhzcSzfRQlBU0dxAjZcsUw
3.refresh_token:当JWT令牌快过期时使用刷新令牌可以再次生成JWT令牌
4.expires_in:过期时间(秒)
5.scope:令牌的权限范围,服务端可以根据令牌的权限范围去对令牌授权
6.jti:令牌的唯一标识
我们可以通过check_token接口校验JWT令牌
###校验jwt令牌
POST {{auth_host}}/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJ
dLCJ1c2VyX25hbWUiOiJzdHUxIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTY2
NDM3MTc4MCwiYXV0aG9yaXRpZXMiOlsicDEiXSwianRpIjoiZjBhM2NkZWItM
zk5ZC00OGYwLTg4MDQtZWNhNjM4YWQ4ODU3IiwiY2xpZW50X2lkIjoiYzEifQ.
qy46CSCJsH3eXWTHgdcntZhzcSzfRQlBU0dxAjZcsUw
响应如下:
{
"aud": [
"res1"
],
"user_name": "zhangsan",
"scope": [
"all"
],
"active": true,
"exp": 1664371780,
"authorities": [
"p1"
],
"jti": "f0a3cdeb-399d-48f0-8804-eca638ad8857",
"client_id": "c1"
}
实例:携带令牌访问资源服务
拿到了JWT令牌下一步就要携带令牌去访问资源服务中的资源,比如在线教育项目:内容管理服务模块,客户端申请到JWT令牌,携带JWT去内容管理服务查询课程信息,此时内容管理服务要对JWT进行校验,只有JWT合法才可以继续访问,如图所示流程:
案例演示:在内存管理服务(在线教育系统)中配置OAuth2资源服务,并测试携带JWT令牌访问受保护接口,比较有无JWT令牌会产生什么结果
1.在内容管理服务的content-api工程中添加依赖
<!--引入Spring Security和OAuth2支持,使服务能验证JWT令牌并保护API端点-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId> <!--依赖1-->
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId> <!--依赖2-->
</dependency>
加入的依赖用途:
依赖1:继承基本安全功能(认证,授权)
依赖2:集成OAuth2协议,支持资源服务器配置
2.在内容管理服务的content-api中添加TokenConfig配置类
目的:用于配置JWT相关组件 — JWT的签名密钥和存储方式
@Configuration
public class TokenConfig {
private String SIGNING_KEY = "mq123";
@Bean
public JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
}
3.添加资源服务配置类ResourceServerConfig
目的:配置资源服务器的安全规则,通过@EnableResourceServer启动资源服务器的功能
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "xuecheng-plus";
@Autowired
TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(RESOURCE_ID)
.tokenStore(tokenStore)
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 禁用 CSRF 保护
.authorizeRequests() //配置对请求的授权策略
.antMatchers("/r/**", "/course/**").authenticated() // 指定 "/r/" 和 "/course/" 这两个路径需要进行身份认证才能访问。
.anyRequest().permitAll(); // 允许所有其他请求(除了上面指定的路径之外)都可以被访问,不需要进行身份认证。
}
}
其中.antMatchers("/r/**", "/course/**").authenticated() ,这些路径请求必须携带有效令牌,否则返回401 Unauthorized
重启内容管理服务,使用httpclient测试:
1.访问根据课程id查询课程接口
### 查询课程信息
GET http://localhost:63040/content/course/2
返回结果:
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
从返回信息可知:当前没有认证(测试未在请求体提供有效的JWT令牌,资源服务器拦截了请求)
下边携带JWT令牌访问接口
1.申请JWT令牌:采用密码模式申请令牌
###### 密码模式
POST {{auth_host}}/auth/oauth/token?client_id=MsWebApp&client_secret=MsWebApp&grant_type=password&username=Kyle&password=123
2.携带JWT令牌访问资源服务地址
### 携带token访问资源服务
GET http://localhost:63040/content/course/2
Authorization: Bearer
//JWT令牌
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX25hbWUiOiJ6a
GFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzMzM0OTgsImF1dGhvcml0aWVzIjpbInA
xIl0sImp0aSI6IjhhM2M2OTk1LWU1ZGEtNDQ1Yy05ZDAyLTEwNDFlYzk3NTkwOSIsImN
saWVudF9pZCI6ImMxIn0.73eNDxTX5ifttGCjwc7xrd-Sbp_mCfcIerI3lGetZto
3.如果携带JWT令牌,且JWT令牌正确,则正常访问资源服务的内容
{
"id": 129,
"companyId": 12293202020,
"companyName": null,
"name": "臭臭",
"users": "君子 ",
"tags": "",
"mt": "1-5",
"st": "1-5-4",
"grade": "204003",
"teachmode": "200002",
"description": null,
"pic": "/mediafiles/2023/03/03/76ac562669dc346992af9dd039060e7b.jpg",
"createDate": "2025-02-25 17:17:07",
"changeDate": "2025-02-26 11:09:31",
"createPeople": null,
"changePeople": null,
"auditStatus": "203002",
"status": "203001",
"charge": "201000",
"price": 0.0,
"originalPrice": null,
"qq": "",
"wechat": "",
"phone": "",
"validDays": 365,
"mtName": "人工智能",
"stName": "计算机科学"
}
如果不正确则报令牌无效的错误,例如:
{
"error": "invalid_token",
"error_description": "Cannot convert access token to JSON"
}
测试获取用户身份
JWT令牌中同样也记录了用户身份信息,当客户端携带JWT访问资源服务,资源服务验证签名,通过后将前两部分的内容还原即可取出用户的身份信息,并将用户身份信息放在了SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份
以在线教育系统查看课程接口为例,进入查询课程接口的代码中,添加获取用户身份的代码
@ApiOperation("根据课程id查询课程基础信息")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable("courseId") Long courseId){
//取出当前用户身份
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("当前用户身份为:"+principal);
return courseBaseInfoService.getCourseBaseInfo(courseId);
}
测试时需要注意:
1.首先在资源服务配置中指定安全拦截机制/course/开头的请求需要认证,既请求/course/{courseId}接口需要携带JWT令牌且签证通过
2.认证服务生成JWT令牌将用户身份信息写入令牌,目前还是将用户信息硬编码并暂放到内存中
如下:
@Bean
public UserDetailsService userDetailsService() {
//这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("liwenwen").password("123").authorities("p1").build());
manager.createUser(User.withUsername("yiyangyang").password("456").authorities("p2").build());
return manager;
}
3.我们在使用密码模式生成JWT令牌时用的是lliwenwen的信息,所以JWT令牌中存储了liwenwen的信息,那么在资源服务中应该取出liwenwen的信息才对
了解了这些内容,使用HttpClient测试接口重启内容管理服务,跟踪取到的用户身份是正确的,结果如下:
当前用户身份为:liwenwen
至此,用户登录通过了认证服务颁发了JWT令牌,客户端携带JWT访问资源服务,资源服务对JWT的合法性进行验证,如下图:
网关 — 完善架构
但是这样的操作流程似乎遗漏了架构中非常重要的组件:网关,加上之后并完善后如下图所示
注:所有访问微服务的请求都要经过网关,在网关进行用户身份的认证可以将很多非法的请求拦截到微服务以外,这叫做网关认证
网关的职责:
1.网络白名单维护:针对不用认证的URL全部放行
2.校验JWT的合法性:除了白名单剩下的就是需要认证的请求,网关需要验证JWT的合法性,JWT合法则说明用户身份合法,否则说明身份不合法则拒绝继续访问
网关负责授权码?
答:网关不负责授权,对请求的授权操作在各个微服务进行,因为微服务最清楚用户有哪些权限访问哪些接口
下面实现网关认证
1.在网关工程添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
2.将网关鉴权配置类添加在项目的config包下
GatewayAuthFilter类:
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {
//白名单
private static List<String> whitelist = null;
static {
//加载白名单 白名单中的路径无需认证即可访问(如登录接口,静态资源)
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {
Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist = new ArrayList<>(strings);
} catch (Exception e) {
log.error("加载/security-whitelist.properties出错:{}", e.getMessage());
e.printStackTrace();
}
}
@Autowired
private TokenStore tokenStore;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestUrl = exchange.getRequest().getPath().value();
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {
if (pathMatcher.match(url, requestUrl)) {
return chain.filter(exchange);
}
}
//检查token是否存在
String token = getToken(exchange);
if (StringUtils.isBlank(token)) {
return buildReturnMono("没有认证", exchange);
}
//判断是否是有效的token
OAuth2AccessToken oAuth2AccessToken;
try {
oAuth2AccessToken = tokenStore.readAccessToken(token);
boolean expired = oAuth2AccessToken.isExpired();
if (expired) {
return buildReturnMono("认证令牌已过期", exchange);
}
return chain.filter(exchange);
} catch (InvalidTokenException e) {
log.info("认证令牌无效: {}", token);
return buildReturnMono("认证令牌无效", exchange);
}
}
/**
* 获取token
*/
private String getToken(ServerWebExchange exchange) {
String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {
return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {
return null;
}
return token;
}
private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(new RestErrorResponse(error));
byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return 0;
}
}
(该过滤器用于拦截所有经过网关的请求,检查请求是否在白名单中,或者是否携带有效的JWT令牌)
1.如果请求在白名单中,直接放行
2.如果请求不在白名单中,检查是否携带有效的JWT令牌
令牌有效 -----> 放行
令牌无效或过期 -----> 返回401 Unauthorized错误
RestErrorResponse类:
(用于封装错误响应信息,在RESTful API中返回统一的错误格式)
public class RestErrorResponse implements Serializable {
private String errMessage;
public RestErrorResponse(String errMessage){
this.errMessage= errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
}
SecurityConfig类
(通过@EnableWebFluxSecurity注解启动了Spring WebFlux的安全功能,并定义一个SecurityWebFilterChain Bean,用于配置请求的安全拦截规则)
@EnableWebFluxSecurity
@Configuration
public class SecurityConfig {
//安全拦截配置
@Bean
public SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) {
return http.authorizeExchange()
.pathMatchers("/**").permitAll()
.anyExchange().authenticated()
.and().csrf().disable().build();
}
}
TokenConfig类
(该Spring配置类用于配置JWT相关的组件,包括TokenStore和JwtAccessTokenConverter)
@Configuration
public class TokenConfig {
String SIGNING_KEY = "mq123";
//定义了TokenStore Bean,用来存储和管理JWT令牌
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
//用于将OAuth2访问令牌与JWT进行转换
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SIGNING_KEY);
return converter;
}
}
配置白名单文件security-whitelist.properties
/auth/**=认证地址
/content/open/**=内容管理公开访问接口
/media/open/**=媒资管理公开访问接口
重启网关工程,进行测试
1.申请令牌
2.通过网关访问资源服务
这里访问内容管理服务
### 通过网关访问资源服务
GET http://localhost:63010/content/course/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzMSJdLCJ1c2VyX
25hbWUiOiJ6aGFuZ3NhbiIsInNjb3BlIjpbImFsbCJdLCJleHAiOjE2NjQzNjIzMTAsImF1dGhvcml0aWVzIjpbInAxIl0sImp0aSI6Ijc2OTkwMGNiLWM1ZjItNGRiNC1hZWJmLWY1MzgxZDQxZWMyZCIsImNsaWVudF9pZCI6ImMxIn0.lOITjUgYg2HCh5mDPK9EvJJqz-tIupKVfmP8yWJQIKs
当token正确时就可以正常访问资源服务,token验证失败返回token无效
{
"errMessage": "认证令牌无效"
}
至此,了解了使用Spring Security进行认证授权的过程,本篇只对Spring Security做了一个简单的介绍,要掌握并结合各种认证方式实现系统的登录认证模块开发,还需要参考一些实例