第四阶段14-关于Token,关于JWT
创始人
2024-06-02 19:24:38
0

关于Session

服务器端的程序通常是基于HTTP协议的,HTTP协议本身是一种“无状态”的,所以,它并不能保存客户端的状态,也就是说,当同一个客户端多次访问同一个服务器时,服务器并不能识别后续访问其实和前序的访问来自同一个客户端。

在开发实际中,需要识别客户端的身份,所以,在编程技术上,可以使用Session机制来解决此问题。

Session的本质是存储在服务器端的内存中的一个K-V结构的数据,服务器端会为每一个来访的客户端的首次访问分配一个Session ID(是一个UUID值,如果客户端的请求没有携带Session ID,则分配,如果已携带Session ID,则不分配),此Session ID就是客户端访问Session数据时使用的Key,所以,每个客户端在服务器端都有一份对应的Session数据(K-V中的Value)。

由于Session是存储在服务器端的内存中的,内存是非常重要的,且容量相对较小的存储设备,所以,必须要设置一种清除Session的机制,默认的清除机制就是“超时自动清除”,即某个客户端在最后一次提求的多长时间内没有再次提交请求(默认的超时时间一般是15分钟或30分钟),则服务器端将自动清除此客户端对应的Session数据。

由于Session是存储在服务器端的内存中的,所以会存在一些缺点:

  • 不合适存储大量数据
    • 应该通过规范的开发,避免此问题
  • 不便于应用到集群或分布式系统中
    • 可以通过共享Session解决此问题
  • 不可以长时间的存储
    • 对于Session机制是无解的

关于Token

**Token:**令牌,或票据

在使用Token机制时,当客户端向服务器端第1次提交请求时,或提交登录请求时,客户端不需要做特殊的处理,而服务器端会在识别此客户端身份后,会将客户端的身份数据生成为Token,并将此Token响应到客户端去,后续,客户端需要携带此Token提交各种请求,服务器端会根据Token中的数据来识别客户端的身份。

在处理过程中,服务器端只需要具体检查Token、从Token中解析出客户端身份的相关数据即可,并不需要在服务器端保存各Token数据,所以,Token是可以设置较长时间的有效期的,不会消耗服务器端存储资源!

同时,Token天生就适用于集群或分布式系统,因为各服务器只需要具有相同的验证、解析Token的程序即可识别客户端的身份。

其实,Token的传输流程与Session ID基本上是相同的,最大的区别在于Session ID并没有实际的数据含义,它只是一段无意义的、可以保证唯一性的随机数据而已,而Token是具有数据含义的,是有意义的数据!

关于JWT

**JWT:**JSON Web Token

关于JWT的官网:https://jwt.io/

每个JWT数据都包含3个组成部分:

  • Header:关于算法与Token类型的声明
  • Payload:数据
  • Signature:验证签名

关于JWT编程的工具包:https://jwt.io/libraries?language=Java

例如,在项目的pom.xml中添加依赖项:


io.jsonwebtokenjjwt0.9.1

接下来,就可以在项目中尝试生成、解析JWT:

package cn.tedu.csmall.passport;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JwtTests {String secretKey = "8ugLIU$#%^*&dlii9MutjKJoHhldSF)JFDL*urfda(&%&^invjfdsa";@Testvoid generate() {Date date = new Date(System.currentTimeMillis() + 5 * 60 * 1000);Map claims = new HashMap<>();claims.put("id", 9527);claims.put("username", "Zhangsan");String jwt = Jwts.builder()// Header.setHeaderParam("alg", "HS256").setHeaderParam("typ", "JWT")// Payload.setClaims(claims)// Signature.setExpiration(date).signWith(SignatureAlgorithm.HS256, secretKey)// 完成.compact();System.out.println(jwt);}@Testvoid parse() {String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6OTUyNywiZXhwIjoxNjc1ODM3Mzc3LCJ1c2VybmFtZSI6IlpoYW5nc2FuIn0._9XtdKoj5CmNEW99dYE9FZLGPoN12pOQHhMr1PLQLs0";Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();Object id = claims.get("id");Object username = claims.get("username");System.out.println("id=" + id);System.out.println("username=" + username);}}

当尝试解析的JWT已经过期时,会出现错误:

io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-02-08T14:12:35Z. Current time: 2023-02-08T14:16:45Z, a difference of 250374 milliseconds.  Allowed clock skew: 0 milliseconds.

当尝试解析的JWT数据格式错误时,会出现错误:

io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"id":9525�"exp":1676701619,"username":"Zhangsan"}

当尝试解析的JWT验证签名错误时,会出现错误:

io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

**注意:**JWT数据是可能被客户端篡改的,所以,当解析失败时,应该不信任对此JWT数据!并且,即使不知道生成JWT时的secretKey的情况下,仍有很多办法可以解析出JWT中的内容,所以,不要在JWT中存入敏感数据。

在项目中使用JWT识别用户的身份

核心流程概述

在项目中使用JWT识别用户的身份,大致需要:

  • 当用户通过认证(登录成功)后,服务器端应该生成此用户对应的JWT数据,并响应到客户端
    • 当视为通过认证后,不再需要将用户的相关信息存入到SecurityContext
  • 当用户尝试执行需要认证的操作时,用户应该携带JWT数据,服务器端应该解析此JWT数据,从而验证JWT的真伪,并识别用户的身份,将用户的相关信息存入到SecurityContext

认证成功后响应JWT

首先,在AdminServiceImpl中执行认证且通过认证后,不再向SecurityContext中存入认证信息:

@Override
public void login(AdminLoginDTO adminLoginDTO) {// 暂不关心前序代码// ========== 删除以下代码 ==========// 将认证信息存入到SecurityContextSecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(authenticationResult);
}

然后,将IAdminService中的登录方法的返回值类型改成String,表示此方法在处理认证成功后,将返回JWT(String类型)数据:

String login(AdminLoginDTO adminLoginDTO);

同时,也将AdminServiceImpl中的登录方法的返回值类型的声明做同样的调整,并在通过认证后生成JWT数据、返回JWT数据:

@Override
public String login(AdminLoginDTO adminLoginDTO) {log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);// 执行认证Authentication authentication = new UsernamePasswordAuthenticationToken(adminLoginDTO.getUsername(), adminLoginDTO.getPassword());Authentication authenticationResult= authenticationManager.authenticate(authentication);log.debug("认证通过,认证结果:{}", authenticationResult);log.debug("认证通过,认证结果中的当事人:{}", authenticationResult.getPrincipal());// =========== 新增以下代码 ==========// 将通过认证的管理员的相关信息存入到JWT中// 准备生成JWT的相关数据String secretKey = "8ugLIU$#%^*&dlii9MutjKJoHhldSF)JFDL*urfda(&%&^invjfdsa";Date date = new Date(System.currentTimeMillis() + 10 * 24 * 60 * 60 * 1000);AdminDetails principal = (AdminDetails) authenticationResult.getPrincipal();Map claims = new HashMap<>();claims.put("id", principal.getId());claims.put("username", principal.getUsername());// 生成JWTString jwt = Jwts.builder()// Header.setHeaderParam("alg", "HS256").setHeaderParam("typ", "JWT")// Payload.setClaims(claims)// Signature.setExpiration(date).signWith(SignatureAlgorithm.HS256, secretKey)// 完成.compact();// 返回JWTreturn jwt;
}

AdminController中处理登录时,调用Service方法时获取返回值,并响应到客户端:

@PostMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO) {log.debug("开始处理【管理员登录】的请求,参数:{}", adminLoginDTO);String jwt = adminService.login(adminLoginDTO);return JsonResult.ok(jwt);
}

通过API文档测试访问,当登录成功后,响应的结果例如:

{"state": 20000,"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MywiZXhwIjoxNjc2NzA0NjM4LCJ1c2VybmFtZSI6ImxpdWNhbmdzb25nIn0.9YOVO3nIEewCCUIn5aon_wOJcbFaaHLwpDrOSeA36v4"
}

解析客户端携带的JWT

客户端提交若干种不同请求时,可能都需要携带JWT,在服务器,处理若干种不同的请求之前也都需要获取并尝试解析JWT,则应该使用**过滤器(Filter)**组件进行处理!

提示:过滤器是Java服务器端组件中,最早接收到请求的组件,它执行在其它任何组件之前!在同一个项目中,允许存在若干个过滤器,形成过滤器链(Filter Chain)!任何一个请求,必须被所有过滤器“放行”才可以被处理!

则在项目的根包下创建filter.JwtAuthorizationFilter类,继承自OncePerRequestFilter,并在类上添加组件注解:

@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {}}

首先,需要尝试接收客户端携带的JWT:

@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 根据业内惯用的做法,客户端应该将JWT保存在请求头(Request Header)中的名为Authorization的属性中String jwt = request.getHeader("Authorization");log.debug("尝试接收客户端携带的JWT数据,JWT:{}", jwt);}}

然后,在SecurityConfiguration中自动装配以上过滤器对象:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {// 新增@Autowiredprivate JwtAuthorizationFilter jwtAuthorizationFilter;// 暂不关心其它代码}

并且,在此配置类的void configuere(HttpSecurity http)方法中,添加此过滤器:

http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)

在API文档中,通过“全局参数设置”中的“添加参数”,可以配置每个请求都将携带JWT数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cLAP4VWi-1677551998237)(images/image-20230208155152295.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hUzi9VvS-1677551998239)(images/image-20230208155320701.png)]

当通过API文档的调试发出任何请求,在API文档界面中看到的结果都将是一片空白,并且,在服务器端的控制台中可以看到输出了对应的JWT数据!

接下来,应该在JwtAuthorizationFilter中尝试解析JWT,并将解析成功时得到的数据用于创建认证对象,再把认证对象存入到SecurityContext中:

package cn.tedu.csmall.passport.filter;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;/*** 

处理JWT的过滤器类

**

此过滤器类的主要职责

*
    *
  • 接收客户端可能提交的JWT
  • *
  • 尝试解析客户端提交的JWT
  • *
  • 将解析得到的结果存入到SecurityContext中
  • *
** @author java@tedu.cn* @version 0.0.1*/ @Slf4j @Component public class JwtAuthorizationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 根据业内惯用的做法,客户端应该将JWT保存在请求头(Request Header)中的名为Authorization的属性中String jwt = request.getHeader("Authorization");log.debug("尝试接收客户端携带的JWT数据,JWT:{}", jwt);// 判断客户端是否提交了有效的JWTif (!StringUtils.hasText(jwt) || jwt.length() < 113) {// 直接放行filterChain.doFilter(request, response);// 【重要】终止当前方法的执行,不执行当前方法接下来的代码return;}// 尝试解析JWTString secretKey = "8ugLIU$#%^*&dlii9MutjKJoHhldSF)JFDL*urfda(&%&^invjfdsa";Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();Object id = claims.get("id");Object username = claims.get("username");log.debug("从JWT中解析得到的管理员ID:{}", id);log.debug("从JWT中解析得到的管理员用户名:{}", username);// 基于解析JWT的结果创建认证信息Object principal = username; // 使用用户名作为当事人数据(临时)Object credentials = null; // 应该为nullList authorities = new ArrayList<>();SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("暂时给个山寨的权限");authorities.add(simpleGrantedAuthority);Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);// 将认证信息存入到SecurityContext中SecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(authentication);// 过滤器链继承向后执行,即:放行// 如果没有执行以下代码,表示“阻止”,即此请求的处理过程到此结束,在浏览器中将显示一片空白filterChain.doFilter(request, response);}}

为了便于观察测试结果,应该暂时取消控制器类中的:

  • 检查权限
  • 注入当事人信息

即临时注释掉一部分代码:

// @PreAuthorize("hasAuthority('/ams/admin/read')")
@GetMapping("")
// public JsonResult> list(@ApiIgnore @AuthenticationPrincipal AdminDetails adminDetails) {
public JsonResult> list() {log.debug("开始处理【查询管理员列表】的请求,参数:无");// log.debug("当事人:{}", adminDetails);// log.debug("当事人的ID:{}", adminDetails.getId());// log.debug("当事人的用户名:{}", adminDetails.getUsername());List list = adminService.list();return JsonResult.ok(list);
}

完成后,只要携带有效的JWT,即可访问,如果未携带有效的JWT,将禁止访问(可能需要重启服务器端)。

关于secretKey的定义

应该将secretKey定义在配置文件(application.yml系列文件)中,以便于统一管理此值,并且,客户可以修改此值(如果定义在.java文件中,经过编译后,此值将无法修改)。

application-dev.yml中添加自定义配置:

# 当前项目中的自定义配置
csmall:# 与JWT相关的配置jwt:# 生成和解析JWT时使用的secret-keysecret-key: 8ugLIU$#%^*&dlii9MutjKJoHhldSF)JFDL*urfda(&%&^invjfdsa# JWT的有效时长duration-in-minute: 14400

JwtAuthorizationFilter中使用此配置:

@Value("${csmall.jwt.secret-key}")
private String secretKey;// 注意:删除原代码中解析JWT时使用的同名的局部变量

AdminServiceImpl中作类似的处理!具体代码参考老师的源文件!

ion-dev.yml`中添加自定义配置:

# 当前项目中的自定义配置
csmall:# 与JWT相关的配置jwt:# 生成和解析JWT时使用的secret-keysecret-key: 8ugLIU$#%^*&dlii9MutjKJoHhldSF)JFDL*urfda(&%&^invjfdsa# JWT的有效时长duration-in-minute: 14400

JwtAuthorizationFilter中使用此配置:

@Value("${csmall.jwt.secret-key}")
private String secretKey;// 注意:删除原代码中解析JWT时使用的同名的局部变量

AdminServiceImpl中作类似的处理!具体代码参考老师的源文件!

相关内容

热门资讯

Python|位运算|数组|动... 目录 1、只出现一次的数字(位运算,数组) 示例 选项代...
张岱的人物生平 张岱的人物生平张岱(414年-484年),字景山,吴郡吴县(今江苏苏州)人。南朝齐大臣。祖父张敞,东...
西游西后传演员女人物 西游西后传演员女人物西游西后传演员女人物 孙悟空 六小龄童 唐僧 徐少华 ...
名人故事中贾岛作诗内容简介 名人故事中贾岛作诗内容简介有一次,贾岛骑驴闯了官道.他正琢磨着一句诗,名叫《题李凝幽居》全诗如下:闲...
和男朋友一起优秀的文案? 和男朋友一起优秀的文案?1.希望是惟一所有的人都共同享有的好处;一无所有的人,仍拥有希望。2.生活,...
戴玉手镯的好处 戴玉手镯好还是... 戴玉手镯的好处 戴玉手镯好还是碧玺好 女人戴玉?戴玉好还是碧玺好点佩戴手镯,以和田玉手镯为佳!相嫌滑...
依然什么意思? 依然什么意思?依然(汉语词语)依然,汉语词汇。拼音:yī    rán基本解释:副词,指照往常、依旧...
高尔基的散文诗 高尔基的散文诗《海燕》、《大学》、《母亲》、《童年》这些都是比较出名的一些代表作。
心在飞扬作者简介 心在飞扬作者简介心在飞扬作者简介如下。根据相关公开资料查询,心在飞扬是一位优秀的小说作者,他的小说作...
卡什坦卡的故事赏析? 卡什坦卡的故事赏析?讲了一只小狗的故事, 我也是近来才读到这篇小说. 作家对动物的拟人描写真是惟妙...
林绍涛为简艾拿绿豆糕是哪一集 林绍涛为简艾拿绿豆糕是哪一集第三十二集。 贾宽认为是阎帅间接导致刘映霞住了院,第二天上班,他按捺不...
小爱同学是女生吗小安同学什么意... 小爱同学是女生吗小安同学什么意思 小爱同学,小安同学说你是女生。小安是男的。
内分泌失调导致脸上长斑,怎么调... 内分泌失调导致脸上长斑,怎么调理内分泌失调导致脸上长斑,怎么调理先调理内分泌,去看中医吧,另外用好的...
《魔幻仙境》刺客,骑士人物属性... 《魔幻仙境》刺客,骑士人物属性加点魔幻仙境骑士2功1体质
很喜欢她,该怎么办? 很喜欢她,该怎么办?太冷静了!! 太理智了!爱情是需要冲劲的~不要考虑着考虑那~否则缘...
言情小说作家 言情小说作家我比较喜欢匪我思存的,很虐,很悲,还有梅子黄时雨,笙离,叶萱,还有安宁的《温暖的玄》 小...
两个以名人的名字命名的风景名胜... 两个以名人的名字命名的风景名胜?快太白楼,李白。尚志公园,赵尚志。
幼儿教育的代表人物及其著作 幼儿教育的代表人物及其著作卡尔威特的《卡尔威特的教育》,小卡尔威特,他儿子成了天才后写的《小卡尔威特...
海贼王中为什么说路飞打凯多靠霸... 海贼王中为什么说路飞打凯多靠霸气升级?凯多是靠霸气升级吗?因为之前刚到时确实打不过人家因为路飞的实力...
运气不好拜财神有用吗运气不好拜... 运气不好拜财神有用吗运气不好拜财神有没有用1、运气不好拜财神有用。2、拜财神上香前先点蜡烛,照亮人神...