【分布式微服务专题】SpringSecurity快速入门
目录
- 前言
- 阅读对象
- 阅读导航
- 前置知识
- 笔记正文
- 一、Spring Security介绍
- 1.1 什么是Spring Security
- 1.2 它是干什么的
- 1.3 Spring Security和Shiro比较
- 二、快速开始
- 2.1 用户认证
- 2.1.1 设置用户名
- 2.1.1.1 基于application.yml配置文件
- 2.1.1.2 基于Java Config配置方式
- 2.1.2 设置加密方式
- 2.1.2.1 {id}encodedPassword
- 2.1.2.2 使用PasswordEncoder加密
- 2.1.3 自定义用户信息加载
- 2.1.4 自定义登录页面
- 2.1.5 前后端分离认证
- 2.1.6 用户认证流程总结
- 2.2 访问控制
- 2.2.1 web授权: 基于url的访问控制
- 2.2.2 方法授权:基于注解的访问控制
- 三、Spring Security整合JWT实现自定义登录认证
- 3.1 自定义登录认证业务流程
- 3.2 JWT介绍
- 3.2.1 什么是JWT
- 3.3 JWT结构
- 3.3.1 JWT头部header
- 3.3.2 JWT载荷payload
- 3.3.3 JWT签名signature
- 3.3.4 组合在一起
- 3.3.5 如何使用
- 3.4 代码实现自定义登录
- 3.5 JWT续期问题
- 3.5.1 刷新令牌(Refresh Token)
- 3.5.2 自动延长JWT有效期
- 学习总结
- 感谢
前言
阅读对象
阅读导航
前置知识
笔记正文
一、Spring Security介绍
1.1 什么是Spring Security
官方介绍:Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于 Spring 的应用程序。
Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring 安全性的真正强大之处,在于它很容易扩展以满足定制需求
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC、DI和AOP功能,类别是安全服务体系。
Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。它可以提供应用程序层的安全解决方案,一个系统的安全还需要考虑传输层和系统层的安全,例如采用Htps协议、服务器部署防火墙等。
此外,Spring Security采用【安全层】的概念,使每一层都尽可能安全,连续的安全层可以达到全面的防护。在Controller层、Service层、DAO层等以加注解的方式来保护应用程序的安全。
1.2 它是干什么的
Spring Security 主要干的就两件事:
Authentication
:认证(who are you)。用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式Access Control
:访问控制(what are you allowed to do)。授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问
SpringSecurity在架构上将认证与授权分离,并提供了扩展点。
1.3 Spring Security和Shiro比较
在 Java 生态中,目前有 Spring Security 和 Apache Shiro 两个安全框架,可以完成认证和授权的功能。
Shiro
:一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理
相同点 | 不同点(以SpringSecurity出发) | |
---|---|---|
SpringSecutiry | 认证功能 授权功能 加密功能 会话管理 缓存支持 rememberMe功能 | 优点: 1. SpringSecurity以Spring为基础,与Spring生态融合有天然的优势 2. SpringSecurity功能更加丰富些,例如安全防护 3. SpringSecurity社区资源比Shiro丰富 缺点: 1. SpringSecurity使用相对复杂,上手难度大 2. SpringSecurity依赖于Spring容器 |
Shiro |
所以,网上有人说,对于常见的安全管理技术栈的组合是:
- SSM + Shiro
- Spring Boot/Spring Cloud +Spring Security
二、快速开始
接下来我们开始使用以下SpringSecutiry,在项目要开始之前,我们需要做一些项目准备。
1)快速新建一个SpringBoot项目
建议直接使用idea的Spring Initializer
云上构建一个
2)添加pom依赖
<!-- 接入spring security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- web配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
3)添加测试接口
@GetMapping("/user/admin")public User admin() {User user = new User();user.setUserId(1);user.setName("admin");user.setPhone("10086");return user;}
4)启动项目,开始测试
引入Spring Security之后 ,访问任何API 接口时,需要首先进行登录,才能进行访问。如下所示:
默认用户名:user,密码可以查看控制台日志获取
输入账号密码之后,访问就正常了
5)退出
Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送/logout
请求即可
OK,准备工作做完之后,接下来开始进入正题。
2.1 用户认证
2.1.1 设置用户名
2.1.1.1 基于application.yml配置文件
配置内容如下:
spring:# Spring Security 配置项,对应 SecurityProperties 配置类security:user:name: userpassword: 123456roles:- admin
这个方式很容易理解,毕竟,我们在前面提到过了,SpringSecurity会有一个默认的用户。然而我们又没有在数据库里面记录它,所以,它肯定是存在于内存中。所以,SpringSecurity提供了这么一种机制给我们去修改它的默认账号密码。
2.1.1.2 基于Java Config配置方式
代码如下:
@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.builder().username("shen").password("{noop}123456").roles("user").build();UserDetails admin = User.builder().username("admin").password("{noop}123456").roles("admin", "user").build();return new InMemoryUserDetailsManager(user, admin);}
}
注意,上面的密码password("{noop}123456")
中的{noop}
是表示不需要加密,明文注册到内存中。至于加密,我会在后面提到。若没有上面这个配置,测试的时候会报错:There is no PasswordEncoder mapped for the id "null"
2.1.2 设置加密方式
2.1.2.1 {id}encodedPassword
这种方式是SpringSecurity提供的一种比较普适性的格式。整体分为两个部分:
{id}
:设置的时候必须在{}
花括号内。id为加密方式。可选的值如下图所示。大家只要记得{noop}
是明文方式就好,其他代表的是不同的加密策略
具体见:org.springframework.security.crypto.factory.PasswordEncoderFactories
encodePassword
:原始密码
简单的使用示例就是我在上个案例写到的,不过在这里我们修改一下shen
用户的密码,采用{sha256}
加密方式,明文为password
:
public class SecurityConfig {@Beanpublic UserDetailsService userDetailsService() {UserDetails user = User.builder().username("shen").password("{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0").roles("user").build();UserDetails admin = User.builder().username("admin").password("{noop}123456").roles("admin", "user").build();return new InMemoryUserDetailsManager(user, admin);}
}
2.1.2.2 使用PasswordEncoder加密
这个使用起来就更简单了,就是先注册声明一个Bean到Spring里面就好,比如使用bcrypt
,通过上图PasswordEncoderFactories
可以看见,需要注册的Bean如下:
@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
2.1.3 自定义用户信息加载
在我们的开发中,通常是需要自定义的方式从数据库获取用户信息的。这种情况下,我们可以自行实现UserDetailsService
接口:
@Service
public class ShenUserService implements UserDetailsService {@Resourceprivate UserService userService;@Resourceprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User byId = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getName, username));return org.springframework.security.core.userdetails.User.builder().username(byId.getName()).password(passwordEncoder.encode("123456")).roles("user").build();}
}
2.1.4 自定义登录页面
Spring Security虽然给我们提供了默认登录页面,但通常我们都会自定义自己的登录页面,在项目中,我们想要自定义登录页面,只需要简单的两步:
1)编写登录页面
在resources
目录下新建static
目录,然后在下面新增login.html
文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><form action="/user/login" method="post">用户名:<input type="text" name="username"/><br/>密码:<input type="password" name="password"/><br/><input type="submit" value="提交"/></form>
</body>
</html>
2)配置Spring Security的过滤器链SecurityFilterChain
@Beanpublic SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(new RequestMatcher() {@Overridepublic boolean matches(HttpServletRequest request) {String requestURI = request.getRequestURI();return "/login.html".equals(requestURI);}}).permitAll() // loginPage 页面不需要身份认证 否则会无限重定向.anyRequest().authenticated() // 其他请求都需要用户认证后访问).formLogin((formLogin) -> formLogin.loginPage("/login.html") // 自定义登录页面路径.usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配.passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配.loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致.defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径)// 禁用httpBasic.httpBasic((httpBasic) -> httpBasic.disable())// 关闭跨站点请求伪造csrf防护.csrf((csrf) -> csrf.disable());return http.build();}
测试一下,你就会发现跳转到了自定义的界面
2.1.5 前后端分离认证
表单登录配置模块提供了successHandler()和failureHandler()两个方法,分别处理登录成功和登录失败的逻辑。携带当前登录用户名及其角色等信息;而failureHandler()方法携带一个AuthenticationException异常参数。
1)新增登录成功处理
/*** 认证成功处理逻辑*/public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {response.setContentType("text/html;charset=utf-8");response.getWriter().write("登录成功");}}
2)新增登录失败处理
/*** 认证失败处理逻辑*/public class LoginFailureHandler implements AuthenticationFailureHandler {@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {// TODOresponse.setContentType("text/html;charset=utf-8");response.getWriter().write("登录失败");exception.printStackTrace();}}
3)设置处理逻辑
修改上一步提到的过滤器链SecurityFilterChain
代码,如下:
.formLogin((formLogin) -> formLogin.loginPage("/login.html") // 自定义登录页面路径.usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配.passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配.loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致.defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径.successHandler(new LoginSuccessHandler()) // 登录成功处理逻辑.failureHandler(new LoginFailureHandler()) // 登录失败处理逻辑
2.1.6 用户认证流程总结
网上找的一个流程图,我跟着走了一遍,大概是这样的,只不过有些类需要自己走一下,确定一下
2.2 访问控制
授权的方式包括 web授权和方法授权,web授权是通过url拦截进行授权,方法授权是通过方法拦截进行授权。
2.2.1 web授权: 基于url的访问控制
Spring Security可以通过http.authorizeRequests()
对web请求进行授权保护 ,Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。配置顺序会影响之后授权的效果,越是具体的应该放在前面,越是笼统的应该放到后面。
代码示例如下:
@Beanpublic SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {// 其余配置http.formLogin((formLogin) -> formLogin.loginPage("/login.html") // 自定义登录页面路径.usernameParameter("username") // 定义从Form中获取用户名的key 与html中的form参数匹配.passwordParameter("password") // 定义从Form中获取密码的key 与html中的form参数匹配.loginProcessingUrl("/user/login") // 认证发起的URL,访问该URL则认证凭证 这样要与HTML中form的提交地址一致.defaultSuccessUrl("/user/admin") // 认证成功之后跳转的路径.successHandler(new LoginSuccessHandler()) // 登录成功处理逻辑.failureHandler(new LoginFailureHandler()) // 登录失败处理逻辑)// 禁用httpBasic.httpBasic(AbstractHttpConfigurer::disable)// 关闭跨站点请求伪造csrf防护.csrf(AbstractHttpConfigurer::disable);// 对请求进行访问控制doSetAccessControl(http);return http.build();}private void doSetAccessControl(HttpSecurity http) throws Exception {// 设置可以直接访问的资源http.authorizeHttpRequests((permitAll) -> permitAll.antMatchers(ignoreUrls).permitAll());// 设置admin特有资源,只有admin可以访问http.authorizeHttpRequests((adminApi) -> adminApi.antMatchers("/sys/**").hasRole("admin"));// 设置wallet接口访问权限http.authorizeHttpRequests((walletApi) -> walletApi.antMatchers("/wallet/**").hasAuthority("wallet:api"));// 其他请求都需要用户认证后访问http.authorizeHttpRequests((others) -> others.anyRequest().authenticated());}
为了方便测试,我写了一个简单的Controller
,然后新增了一些限制API:
@RestController
public class TestController {@GetMapping("/test/get")public String test() {return "获取一个test资源";}@GetMapping("/sys/get")public String sysTest() {return "获取一个sys资源";}@GetMapping("/order/get")public String orderTest() {return "获取一个order资源";}
}
并且,固定我的登录用户只有user:api
的权限:
**/
@Service
public class ShenUserService implements UserDetailsService {@Resourceprivate UserService userService;@Resourceprivate PasswordEncoder passwordEncoder;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User byId = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getName, username));return org.springframework.security.core.userdetails.User.builder().username(byId.getName()).password(passwordEncoder.encode("123456")).authorities("user:api").build();}
}
2.2.2 方法授权:基于注解的访问控制
Spring Security在方法的权限控制上支持三种类型的注解,JSR-250注解、@Secured注解和支持表达式的注解。这三种注解默认都是没有启用的,需要通过@EnableGlobalMethodSecurity来进行启用。
另外,Spring Security中定义了四个支持使用表达式
的注解,分别是@PreAuthorize、@PostAuthorize、@PreFilter和@PostFilter。其中前两者可以用来在方法调用前或者调用后进行权限检查,后两者可以用来对集合类型的参数或者返回值进行过滤。
示例代码如下:
/*** @author zhangshen* @date 2023/12/29 17:16* @slogan 编码即学习,注释断语义**/
@RestController
public class TestController {@PreAuthorize("hasRole('ROLE_order')")@GetMapping("/test/get")public String test() {return "获取一个test资源";}@Secured("ROLE_admin")@GetMapping("/sys/get")public String sysTest() {return "获取一个sys资源";}@Secured("ROLE_order")@GetMapping("/order/get")public String orderTest() {return "获取一个order资源";}
}
三、Spring Security整合JWT实现自定义登录认证
3.1 自定义登录认证业务流程
一个简单易用的登录认证授权,可以使用Spring Security + JWT
实现。在这个框架下,认证授权流程通常如下:
- 用户调用登录接口获取token
- 服务端收到登录请求,校验账号密码
- 校验通过,服务端使用JWT生成token,并返回给用户
- 往后前端每次请求都带上token(在请求头上添加Authorization: Bearer Token)
- 服务端每次收到请求都校验token的合法性
- 校验通过,进行业务逻辑,返回业务结果
大概的UML活动图如下:
3.2 JWT介绍
官方传送门:《JWT介绍》
3.2.1 什么是JWT
JWT即JSON Web Token,它是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的【协议格式】,用于在通信双方【传递json对象】,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。
关键词:【协议格式】、【JSON对象】
它具有如下优点:
JWT令牌的优点:
- jwt基于json,非常方便解析
- 可以在令牌中自定义丰富的内容,易扩展
- 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高
- 源服务使用JWT可不依赖授权服务即可完成授权
缺点:
- JWT令牌较长,占存储空间比较大。
- 安全性取决于密钥管理。JWT 的安全性取决于密钥的管理。如果密钥被泄露或者被不当管理,那么 JWT 将会受到攻击。因此,在使用 JWT 时,一定要注意密钥的管理,包括生成、存储、更新、分发等等
- 无法撤销。由于 JWT 是无状态的,一旦 JWT 被签发,就无法撤销。如果用户在使用 JWT 认证期间被注销或禁用,那么服务端就无法阻止该用户继续使用之前签发的 JWT。因此,开发人员需要设计额外的机制来撤销 JWT,例如使用黑名单或者设置短期有效期等等。
使用 JWT 主要用来做下面两点:
- 认证(Authorization):这是使用 JWT 最常见的一种情况,一旦用户登录,后面每个请求都会包含 JWT,从而允许用户访问该令牌所允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小。
- 信息交换(Information Exchange):JWT 是能够安全传输信息的一种方式。通过使用公钥/私钥对 JWT 进行签名认证。此外,由于签名是使用 head 和 payload 计算的,因此你还可以验证内容是否遭到篡改。
3.3 JWT结构
一个JWT实际上就是一个由.
分隔成三部分的字符串(有点拗口)。这三部分其实就是:头部(header)
、载荷(payload)
与签名(signature)
。
简洁形式的JWT字符串格式就像这样:xxxxxx.yyyyyy.zzzzzz
。具体如下图所示:
3.3.1 JWT头部header
头部用于描述关于该JWT的最基本的信息,通常包含两部分:
- 类型:在这里即JWT
- 签名所用的算法:(如HMACSHA256或RSA)等。
例如:
{"alg": "HS256","typ": "JWT"
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
3.3.2 JWT载荷payload
第二部分是载荷,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:
- 标准中注册的声明。这些是一组预定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的、可互操作的声明。其中包括:iss(发行者)、exp(过期时间)、sub(主题)、aud(受众)等
- 公共的声明。公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密
- 私有的声明。私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
一个简单的示例:
{"sub": "1234567890","name": "John Doe","admin": true
}
然后将其进行base64加密,得到JWT的第二部分:
ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJhZG1pbiI6IHRydWUKfQ==
3.3.3 JWT签名signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret(盐,一定要保密)
这个部分需要base64加密后的header
和base64加密后的payload
使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分:
// base64Header + base64Payload
String encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);// 以zhangshen作【盐】值,对上面的字符串做HS256加密
String signature = HMACSHA256(encodedString, 'zhangshen'); // khA7TNYc7_0iELcDyTc7gHBZ_xfIcgbfpzUNWwQtzME
3.3.4 组合在一起
将上面三部生成的三部分组成在一起,就构成了一个完整的JWT了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ewogICJzdWIiOiAiMTIzNDU2Nzg5MCIsCiAgIm5hbWUiOiAiSm9obiBEb2UiLAogICJhZG1pbiI6IHRydWUKfQ==.80398fd80672f162495e294c86debfaa9fac06788aa49810c7883451311d9b6d
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
3.3.5 如何使用
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', {headers: {'Authorization': 'Bearer ' + token}
})
3.4 代码实现自定义登录
现在我们来改造实现一下,自定义的JWT登录。需要以下4步:
步骤一:新增JWT工具类
package com.shen.jwt;import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.signers.JWTSignerUtil;import java.util.Map;/*** @author zhanghuitong* @date 2024/1/1 18:13* @slogan 编码即学习,注释断语义**/
public class JwtUtil {private static final String SALT = "zhangshen";public static final Integer EXPIRE = 30;/*** 获取默认过期时间的jwt签名器*/public static JWT getJwt() {return getJwt(EXPIRE);}/*** 获取自定义过期时间的jwt签名器*/public static JWT getJwt(Integer expire) {return getJWT().setExpiresAt(DateUtil.offsetMinute(DateUtil.date(), expire));}/*** 生成jwt签名器** @return*/private static JWT getJWT() {return JWT.create().setSigner(JWTSignerUtil.hs256(SALT.getBytes()));}/*** 根据token生成jwt** @param token* @return*/public static JWT parse(String token) {return getJWT().parse(token);}/*** 解析token*/public static JSONObject parseToken(String token) {return parse(token).getPayloads();}/*** 生成token** @param claims payload声明*/public static String token(Map<String, Object> claims) {return getJwt().addPayloads(claims).sign();}
}
上面比较重要的方法是,生成JWT
以及token
的方法
2)实现校验JWT token的过滤器
package com.shen.jwt;import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;/*** @author zhanghuitong* @date 2024/1/1 17:58* @slogan 编码即学习,注释断语义**/
@Component
public class JwtAuthticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 1. 从请求头获取tokenString token = request.getHeader(HttpHeaders.AUTHORIZATION);if (StrUtil.isEmpty(token)) {filterChain.doFilter(request, response);return;}// 2. 校验tokenString realToken = token.substring("bearer".length());JSONObject tokenObject = JwtUtil.parseToken(realToken);Date expireIn = tokenObject.getDate("expireIn");if (expireIn.before(new Date())) {// token 已经过期SecurityContextHolder.getContext().setAuthentication(null);filterChain.doFilter(request, response);return;}String username = tokenObject.getStr("username");Authentication authentication1 = SecurityContextHolder.getContext().getAuthentication();if (StrUtil.isNotEmpty(username) && authentication1 == null) {// 获取用户信息UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (userDetails != null && userDetails.isEnabled()) {UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 设置用户登录状态SecurityContextHolder.getContext().setAuthentication(authentication);}}filterChain.doFilter(request, response);}
}
3)添加自定义JWT过滤器,并添加到账号密码校验过滤器之前
// 添加JWT过滤器,登录的时候校验tokenJwtAuthticationTokenFilter jwtAuthticationTokenFilter = SpringUtil.getBean(JwtAuthticationTokenFilter.class);http.addFilterBefore(jwtAuthticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
4)最后我们来测试一下
3.5 JWT续期问题
JWT(JSON Web Token)通常是在用户登录后签发的,用于验证用户身份和授权。JWT 的有效期限(或称“过期时间”)通常是一段时间(例如1小时),过期后用户需要重新登录以获取新的JWT。然而,在某些情况下,用户可能会在JWT到期之前使用应用程序,这可能会导致应用程序不可用或需要用户重新登录。为了避免这种情况,通常有两种解决方案来处理JWT续期问题:
3.5.1 刷新令牌(Refresh Token)
刷新令牌是一种机制,它允许应用程序获取一个新的JWT,而无需用户进行身份验证。当JWT过期时,应用程序使用刷新令牌向身份验证服务器请求一个新的JWT,而无需提示用户输入其凭据。这样,用户可以继续使用应用程序,而不必重新登录。
以下是一个Java伪代码,演示如何使用Refresh Token来更新JWT
public String refreshAccessToken(String refreshToken) {// 刷新token校验。检查签名,过期时间等boolean isValid = validateRefreshToken(refreshToken);if (isValid) {// 检索与刷新令牌关联的用户信息(例如用户ID)String userId = getUserIdFromRefreshToken(refreshToken);// 重新生成一个新的tokenString newToken= generateToken(userId);return newAccessToken;} else {throw new RuntimeException("Invalid refresh token.");}
}
在这个示例中,refreshAccessToken
方法接收一个刷新令牌作为参数,并使用validateRefreshToken
方法验证该令牌是否有效。如果令牌有效,方法将使用getUserIdFromRefreshToken
方法获取与令牌关联的用户信息,然后使用generateToken
方法生成一个新的JWT访问令牌,并将其返回。如果令牌无效,则抛出异常。
3.5.2 自动延长JWT有效期
在某些情况下,JWT可以自动延长其有效期。例如,当用户在JWT过期前继续使用应用程序时,应用重新设置token过期时间。
要自动延长JWT有效期,您可以在每次请求时检查JWT的过期时间,并在必要时更新JWT的过期时间。以下是一个示例Java代码,演示如何自动延长JWT有效期:
public String getAccessToken(HttpServletRequest request) {String accessToken = extractAccessTokenFromRequest(request);if (isAccessTokenExpired(accessToken)) {String userId = extractUserIdFromAccessToken(accessToken);accessToken = generateNewAccessToken(userId);} else if (shouldRefreshAccessToken(accessToken)) {String userId = extractUserIdFromAccessToken(accessToken);accessToken = generateNewAccessToken(userId);}return accessToken;
}private boolean isAccessTokenExpired(String accessToken) {// extract expiration time from the access tokenDate expirationTime = extractExpirationTimeFromAccessToken(accessToken);// check if the expiration time is in the pastreturn expirationTime.before(new Date());
}private boolean shouldRefreshAccessToken(String accessToken) {// extract expiration time and current timeDate expirationTime = extractExpirationTimeFromAccessToken(accessToken);Date currentTime = new Date();// calculate the remaining time until expirationlong remainingTime = expirationTime.getTime() - currentTime.getTime();// refresh the token if it expires within the next 5 minutesreturn remainingTime < 5 * 60 * 1000;
}private String generateNewAccessToken(String userId) {// generate a new access token with a new expiration timeDate expirationTime = new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME);String accessToken = generateAccessToken(userId, expirationTime);return accessToken;
}
在这个示例中,getAccessToken方法接收HttpServletRequest对象作为参数,并使用extractAccessTokenFromRequest方法从请求中提取JWT访问令牌。然后,它使用isAccessTokenExpired方法检查JWT的过期时间是否已过期。如果过期,它使用extractUserIdFromAccessToken方法从JWT中提取用户ID,并使用generateNewAccessToken方法生成一个新的JWT访问令牌。如果JWT尚未过期,但即将到期,则使用shouldRefreshAccessToken方法检查JWT是否需要更新。如果是这样,它使用相同的流程生成一个新的JWT访问令牌。
学习总结
- 学习并且了解了什么是JWT
- 学会了JWT基本使用及常见问题的解决思路
感谢
感谢官方文章《JWT介绍》
相关文章:
【分布式微服务专题】SpringSecurity快速入门
目录 前言阅读对象阅读导航前置知识笔记正文一、Spring Security介绍1.1 什么是Spring Security1.2 它是干什么的1.3 Spring Security和Shiro比较 二、快速开始2.1 用户认证2.1.1 设置用户名2.1.1.1 基于application.yml配置文件2.1.1.2 基于Java Config配置方式 2.1.2 设置加密…...
EasyRecovery2024永久免费版电脑数据恢复软件
EasyRecovery是一款操作安全、价格便宜、用户自主操作的非破坏性的只读应用程序,它不会往源驱上写任何东西,也不会对源驱做任何改变。它支持从各种各样的存储介质恢复删除或者丢失的文件,其支持的媒体介质包括:硬盘驱动器、光驱、…...
iphone 苹果 IOS 越狱详细图文保姆级教程非常简单
现在随着各个工具的升级,越狱的难度也是越来越低,还记得 iphone 4 的时候我越狱还是花钱请别人搞得,现在只要你的机型支持越狱,下个工具点一点就可以了,非常简单 目前来说整个越狱过程中,寻找合适机型是最…...
华为HarmonyOS 创建第一个鸿蒙应用 运行Hello World
使用DevEco Studio创建第一个项目 Hello World 1.创建项目 创建第一个项目,命名为HelloWorld,点击Finish 选择Empty Ability模板,点击Next Hello World 项目已经成功创建,接来下看看效果 2.预览 Hello World 点击右侧的预…...
[C#]Onnxruntime部署Chinese CLIP实现以文搜图以文找图功能
【官方框架地址】 https://github.com/OFA-Sys/Chinese-CLIP 【算法介绍】 在当今的大数据时代,文本信息处理已经成为了计算机科学领域的核心议题之一。为了高效地处理海量的文本数据,自然语言处理(NLP)技术应运而生。而在诸多N…...
openssl ans1定义的实体
由于openssl中的ASN1的结构是通过宏来定义的,导致我们经常找不到他的结构在哪里,通过阅读rfc,并且对照OPENSSL,发现OPENSSL中的结构基本是按照相关rfc中的名称,在openssl中进行搜索,就能找到具体的定义了。…...
【Linux Shell】4. 数组
文章目录 【 1. 数组的定义 】【 2. 读取数组 】【 3. 关联数组 】3.1 关联数组的定义3.2 关联数组元素的调用 【 4. 获取数组中的所有元素 】【 5. 获取数组的长度 】 数组中可以存放多个值。 Bash Shell 只支持一维数组(不支持多维数组),初…...
蓝牙运动耳机哪款好用?运动用什么耳机比较好?2024运动耳机推荐
在众多的耳机类型中,运动耳机因其独特的设计和功能而备受青睐。它们不仅要具备出色的音质,还需要能够适应激烈的运动环境,如防水、防汗、牢固耐用等。今天,我想向大家推荐一些在这些方面表现出色的运动耳机,这些耳机…...
XD6500S一款串口SiP模块 射频LoRa芯片 内置sx1262
1.1产品介绍 XD6500S是一款集射频前端和LoRa射频于一体的LoRa SIP模块系列收发器SX1262 senies,支持LoRa⑧和FSK调制。LoRa技术是一种扩频协议优化低数据速率,超长距离和超低功耗用于LPWAN应用的通信。 XD6500S设计具有4.2 mA的有效接收电流消耗&#…...
【华为OD机试真题2023CD卷 JAVAJS】测试用例执行计划
华为OD2023(C&D卷)机试题库全覆盖,刷题指南点这里 测试用例执行计划 时间限制:1s 空间限制:256MB 限定语言:不限 题目描述: 某个产品当前迭代周期内有N个特性()需要进行覆盖测试,每个特性都被评估了对应的优先级,特性使用其ID作为下标进行标识。 设计了M个测试用…...
猫长期吃猫粮好吗?主食冻干猫粮那种好吃又健康
许多铲屎官可能认为,只需给猫咪喂食猫粮就足够了。然而,猫咪实际上是肉食动物,对蛋白质的需求非常高。冻干猫粮采用低温真空干燥处理技术,将鲜肉经过预冻、升华、解析三个过程,去除水分的同时保持蛋白质等营养物质不变…...
计算机毕业设计-----ssm停车位租赁系统
项目介绍 该系统采用了经典的springmvc,spring,mybatis的框架组合,对于物业公司来说,有助于管理车位信息。系统分为了两个角色:车主和租客。 车主主要功能包括: 停车位信息 停车位列表 添加停车位 租赁合…...
Git保姆级安装教程
Git保姆级安装教程 一、去哪下载二、安装2.1 具体安装步骤2.2 设置全局用户签名 一、去哪下载 1、官网(有最新版本):https://git-for-windows.github.io/ 2、本人学习时安装的版本,链接:https://pan.baidu.com/s/1uAo…...
听GPT 讲Rust源代码--compiler(34)
File: rust/compiler/rustc_middle/src/ty/print/mod.rs 在Rust源代码中,文件rust/compiler/rustc_middle/src/ty/print/mod.rs的作用是定义了打印类型和其他相关信息的功能。 具体来说,该文件中定义了三个trait,分别为Print<tcx>、Pri…...
视频融合云平台/智慧监控平台EassyCVR告警警告出错是什么原因?该如何解决?
视频集中存储/云存储/视频监控管理平台EasyCVR能在复杂的网络环境中,将分散的各类视频资源进行统一汇聚、整合、集中管理,实现视频资源的鉴权管理、按需调阅、全网分发、智能分析等。AI智能/大数据视频分析EasyCVR平台已经广泛应用在工地、工厂、园区、楼…...
Gin 路由注册与请求参数获取
Gin 路由注册与请求参数获取 文章目录 Gin 路由注册与请求参数获取一、Web应用开发的两种模式1.前后端不分离模式2.前后端分离模式 二、RESTful介绍三、API接口3.1 RESTful API设计指南3.2 API与用户的通信协议3.3 RestFul API接口设计规范3.3.1 api接口3.3.2 接口文档…...
Linux第11步_解决“挂载后的U盘出现中文乱码”
学习完“通过终端挂载和卸载U盘”,我们发现U盘下的中文文件名会出现乱码,现在讲解怎么解决这个问题。其实就是复习一下“通过终端挂载和卸载U盘”,单独讲解,是为了解决问题,一次性搞好,我们会不长记性。 在…...
【第一节】安装java jdk 21
在 Java Downloads | Oracle 中国 网站下载jdk21的包 查看jdk 命令 /usr/libexec/java_home -V 设置环境变量 配置环境变量 在~/.bash_profile文件里面加入以下环境变量 export JAVA_HOME/Library/Java/JavaVirtualMachines/jdk-21.jdk/Contents/Home export PATH$PATH:$J…...
vue3+echart绘制中国地图并根据后端返回的坐标实现涟漪动画效果
1.效果图 2.前期准备 main.js app.use(BaiduMap, {// ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */ak: sRDDfAKpCSG5iF1rvwph4Q95M6tDCApL,// v:3.0, // 默认使用3.0// type: WebGL // ||API 默认API (使用此模式 BMapBMapGL) });i…...
HCIA-Datacom题库(自己整理分类的)_09_Telent协议【13道题】
一、单选 1.某公司网络管理员希望能够远程管理分支机构的网络设备,则下面哪个协议会被用到? RSTP CIDR Telnet VLSM 2.以下哪种远程登录方式最安全? Telnet Stelnet v100 Stelnet v2 Stelnet v1 解析: Telnet 明文传输…...
Git专栏篇
一、基础知识 二、常用手段 1. 复制其他提交到本分支 目的:现有git仓库,该仓库有两个分支a和b,将a分支的最近三个版本提交内容复制 到b分支的提交上。 在 Linux 系统中,你可以按照以下步骤将分支 A 的最近三个版本的提交内容复…...
Java-字符串-String类
1 需求 1.1 Field Summary 1.2 Constructor Summary public String() : 空构造public String(byte[] bytes) : 把字节数组转成字符串public String(byte[] bytes,int index, int length) : 把字节数组的一部分转成字符串public String(char[] value) : 把字符数组转成字符串p…...
ubuntu安装docker指定版本
ubuntu安装docker指定版本 https://docs.docker.com/engine/install/ubuntu/ 安装apt源 # Add Dockers official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.d…...
说一下 jsp 的 4 种作用域?
说一下 jsp 的 4 种作用域? 在 JSP(JavaServer Pages)中,有四种作用域,它们决定了对象的可见性和生命周期。这四种作用域分别是: 页面作用域(Page Scope): 页面作用域表…...
性能分析与调优: Linux 使用ELRepo升级CentOS内核
目录 一、实验 1.环境 2.agent 服务器使用ELRepo升级CentOS内核 二、问题 1. RHEL-7, SL-7 或者 CentOS-7系统如何安装ELRepo 2.RHEL-8或者RHEL-9系统如何安装ELRepo 一、实验 1.环境 (1)主机 表1-1 主机 主机架构组件IP备注prometheus 监测 系…...
【【RTC实时时钟实验 -- 在HDMI上显示-FPGA 小实验】】
RTC实时时钟实验 – 在HDMI上显示 top.v module RTS_TOP#(parameter TIME_INIT 48h24_01_06_11_08_00 ,parameter WAIT_TIME 13d8000 ,parameter SLAVE_ADDR 7b1010001 , // E2PROM 浠庢満鍦板潃parameter CLK_FR…...
Flutter 图片和资源的高效使用指南
文章目录 指定资源什么是 [pubspec.yaml](https://dart.cn/tools/pub/pubspec) 文件 图片图片常用的配置属性加载本地图片通过 pubspec.yml 文件进行配置图片目录使用 Image.asset 小部件加载本地图片 加载网络图片通过 Image.network小部件加载网络图片:使用Image.…...
RedisTemplate 怎么获取到链接信息?怎么获取到所有key?怎么获取指定key?
获取Redis的链接信息: (RedisTemplate<String, ?> redisTemplate) {RedisConnectionFactory connectionFactory redisTemplate.getConnectionFactory();(!(connectionFactory LettuceConnectionFactory)) {System..println();;}LettuceConnectionFactory l…...
【Unity】动态申请权限
1、AndroidManifest.xml在<application></application>内添加一行: <meta-data android:name"unityplayer.SkipPermissionsDialog" android:value"true" /> 作用:屏蔽应用启动时弹出申请权限弹窗(危…...
tp8/6 插件PhpOffice\PhpSpreadsheet导入表格
一、安装 composer require phpoffice/phpspreadsheet 官网:phpoffice/phpspreadsheet - Packagist 二、代码 <?php namespace app\services\upload\model; use app\services\BaseServices; use \PhpOffice\PhpSpreadsheet\Spreadsheet; use \PhpOffice\Php…...
临沂seo/seo软件排行榜前十名
今天终于又回到学校,这三天的心情有些乱糟糟的。 在北京坐公交时,我看到的是一个大都市,“中国中央电视台”、“中国社会科学院”,这些只有在报纸和网络上才能见到的字眼现在活生生地出现在我的眼前,确实有些震撼。北京…...
wordpress 百度 seo/新产品的推广销售方法
年初的时候老板火急火燎撒钱买了一台四路1080Ti服务器给实验室用作深度学习使用,结果火急火燎地买回来才发现很多深度学习框架对多GPU支持并不好,于是只能分开使用。想我也是经历过共用服务器的混乱,于是提议建立四个虚拟机,然后把…...
移动网站建设服务商/萝卜建站
Spark插入MongoDB出现的错误 解决方案: 1.确认是否在spark中配置了相关的库和表 2.检测是否出现1个以上的spark(我之前检查过配置都没有问题,demo也可以跑,但是我的离线用户画像却一直插入不进去。 仔细检查发现,我写了个sparkSession&am…...
南阳做网站哪个好/小说网站排名人气
PostCSS 本身是一个功能比较单一的工具。它提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理。插件基于 CSS 代码的 AST 所能进行的操作是多…...
国外做ae模板网站大全/如何优化网络环境
在安装vue之前,先确保node和npm是否安装。如果安装ok,那么接下来安装vue1. npm install vue (安装vue)2. npm install --global vue-cli (安装vue-cli)因为npm是国外的,使用起来比较慢,使安装vue会出现报…...
制作一个app的完整流程/国外seo网站
php上传文件并存储到mysql数据库的方法,本文实例讲述了php上传文件并存储到mysql数据库的方法。分享给大家供大家参考。具体分析如下:下面的代码分别用于创建mysql表和上传文件保存到mysql数据库创建mysql表:上传文件并保存到mysql中…...