第四阶段15-关于权限,处理解析JWT时的异常,跨域请求,关于Spring Security的认证流程

news2024/9/22 11:32:07

处理解析JWT时的异常

由于解析JWT是在过滤器中执行的,而过滤器是整个服务器端中最早接收到所有请求的组件,此时,控制器等其它组件尚未运行,则不可以使用此前的“全局异常处理器”来处理解析JWT时的异常(全局异常处理器只能处理控制器抛出的异常),也就是说,在过滤器中只能使用try...catch语法来处理异常!

为了便于表示需要响应到客户端的结果,仍推荐使用JsonResult封装相关信息,而响应到客户端的结果应该是JSON格式的,则需要将JsonResult对象转换成JSON格式的字符串!

在项目中添加fastjson依赖项,此依赖项可以实现Java对象与JSON格式的字符串的相互转换:

<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

在处理异常之前,还应该在ServiceCode中补充相关的业务状态码,例如:

ERR_JWT_EXPIRED(60000),
ERR_JWT_MALFORMED(60100),
ERR_JWT_SIGNATURE(60200),

然后,调整JwtAuthorizationFilter中解析JWT的代码片段:

Claims claims = null;
response.setContentType("application/json; charset=utf-8");
try {
    claims = Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(jwt)
            .getBody();
} catch (SignatureException e) {
    String message = "非法访问!";
    log.warn("解析JWT时出现SignatureException,响应消息:{}", message);
    JsonResult<Void> jsonResult 
        = JsonResult.fail(ServiceCode.ERR_JWT_SIGNATURE, message);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(JSON.toJSONString(jsonResult));
    printWriter.close();
    return;
} catch (MalformedJwtException e) {
    String message = "非法访问!";
    log.warn("解析JWT时出现MalformedJwtException,响应消息:{}", message);
    JsonResult<Void> jsonResult 
        = JsonResult.fail(ServiceCode.ERR_JWT_MALFORMED, message);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(JSON.toJSONString(jsonResult));
    printWriter.close();
    return;
} catch (ExpiredJwtException e) {
    String message = "您的登录信息已过期,请重新登录!";
    log.warn("解析JWT时出现ExpiredJwtException,响应消息:{}", message);
    JsonResult<Void> jsonResult 
        = JsonResult.fail(ServiceCode.ERR_JWT_EXPIRED, message);
    PrintWriter printWriter = response.getWriter();
    printWriter.println(JSON.toJSONString(jsonResult));
    printWriter.close();
    return;
}

关于认证信息中的“当事人”

认证信息中的“当事人”可以通过@AuthenticationPrincipal注入到方法(控制器的方法)的参数中,在访求处理过程中,可能需要获取“当事人”的ID、用户名等数据,所以,应该自定义数据类型表示“当事人”(Spring Security也希望你这样做,所以,在认证结果的信息中的“当事人”被声明为Object类型)!

在项目的根包下创建security.LoginPrincipal类:

package cn.tedu.csmall.passport.security;

import lombok.Data;

import java.io.Serializable;

/**
 * 当事人类型,就是成功通过认证的用户信息类型
 * 
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Data
public class LoginPrincipal implements Serializable {

    /**
     * 当事人ID
     */
    private Long id;
    /**
     * 当事人用户名
     */
    private String username;

}

然后,调整JwtAuthorizationFilter中的相关代码:

Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
log.debug("从JWT中解析得到的管理员ID:{}", id);
log.debug("从JWT中解析得到的管理员用户名:{}", username);

// 基于解析JWT的结果创建认证信息
LoginPrincipal principal = new LoginPrincipal();
principal.setId(id);
principal.setUsername(username);

// 暂不关心其它代码

在控制器中处理请求的方法的参数列表中,可以通过@AuthenticationPrincipal注入LoginPrincipal类型的参数,在处理请求的过程中,可以通过此参数获取当事人的具体数据:

@GetMapping("")
public JsonResult<List<AdminListItemVO>> list(
    	//                                  ↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 自定义的当事人类型
    	@ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
    log.debug("开始处理【查询管理员列表】的请求,参数:无");
    log.debug("当事人:{}", loginPrincipal);
    log.debug("当事人的ID:{}", loginPrincipal.getId());
    log.debug("当事人的用户名:{}", loginPrincipal.getUsername());
    List<AdminListItemVO> list = adminService.list();
    return JsonResult.ok(list);
}

关于权限

目前,当管理员登录成功后,服务器端生成的JWT中并不包含此管理员的权限信息,所以,此管理员后续携带JWT提交请求时,服务器端也无法根据此JWT直接获取管理员的权限列表,更加不能把正确的权限列表存入到SecurityContext中的认证信息中,所以,无法检查管理员的权限!

可以在管理员登录成功后,将此管理员的权限列表也写入到JWT中!

**注意:**由于权限列表并不是一般的字面值数据,生成到JWT中后,将无法还原回原本的集合类型!应该先将权限列表转换成JSON格式的字符串,后续,再根据此JSON数据还原回原本的列表!

AdminServiceImpl中的login()方法中调整:

// 将通过认证的管理员的相关信息存入到JWT中
// 准备生成JWT的相关数据
Date date = new Date(System.currentTimeMillis() + durationInMinute * 60 * 1000);
AdminDetails principal = (AdminDetails) authenticationResult.getPrincipal();
Map<String, Object> claims = new HashMap<>();
claims.put("id", principal.getId());
claims.put("username", principal.getUsername());
Collection<GrantedAuthority> authorities = principal.getAuthorities(); // 新增
String authoritiesJsonString = JSON.toJSONString(authorities); // 新增
claims.put("authoritiesJsonString", authoritiesJsonString); // 新增

然后,在JwtAuthorizationFilter中调整:

Long id = claims.get("id", Long.class);
String username = claims.get("username", String.class);
String authoritiesJsonString = claims.get("authoritiesJsonString", String.class); // 新增
log.debug("从JWT中解析得到的管理员ID:{}", id);
log.debug("从JWT中解析得到的管理员用户名:{}", username);
log.debug("从JWT中解析得到的管理员权限列表JSON:{}", authoritiesJsonString);

// ========== 将原有的“山寨”权限的代码替换为以下代码 ==========
// 将JSON格式的权限列表转换成Authentication需要的类型(Collection<GrantedAuthority>)
List<SimpleGrantedAuthority> authorities =
    JSON.parseArray(authoritiesJsonString, SimpleGrantedAuthority.class);

最后,在控制器中,可以继续使用@PreAuthroize注解检查权限!

**注意:**在API文档的调试中,需要使用新的JWT数据!

关于清空SecurityContext

目前,当使用有效的JWT提交请求后,即使下一次(间隔时间不能太久)不携带JWT再次提交请求,也是服务器端认可的!

这是因为:当使用有效的JWT提交请求后,JwtAuthorizationFilter会将此JWT中的信息创建为Authentication对象,并将此Authentication保存到SecurityContext中!而SecurityContext是基于Session的,所以,在Session未过期之前,即使后续的请求没有携带JWT,Spring Security仍能从Session中找到SecurityContext中的Authentication对象,会视为此次访问与此前的访问是同一个客户端。

如果认为这是一种不合理的表现,可以在JwtAuthorizationFilter中,在执行过滤之初,就不由分说的**清空SecurityContext**即可!

// 清除SecurityContext中的数据
SecurityContextHolder.clearContext();

处理未登录的错误

当客户端没有携带JWT,却向需要通过认证的URL发起请求,默认将响应403错误,这种问题需要在Spring Security的配置类中的void configure(HttpSecurity http)方法中进行配置:

// 处理“需要通过认证,但是实际上未通过认证就发起的请求”导致的错误
http.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        String message = "未检测到登录,请登录!(在开发阶段,看到此提示时,请检查客户端是否携带了JWT向服务器端发起请求)";
        JsonResult<Void> jsonResult = JsonResult.fail(ServiceCode.ERR_UNAUTHORIZED, message);
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        printWriter.println(JSON.toJSONString(jsonResult));
        printWriter.close();
    }
});

关于Spring Security的认证流程

请添加图片描述

使用axios携带JWT发起请求

当需要携带JWT发起请求时,根据业内惯例,JWT应该放在请求头的Authorization属性中,则使用axios时需要自定义请求头,其语法是:

// 以下调用的create()表示创建一个axios实例,此函数的参数是一个对象,用于表示新创建的axios的参数
this.axios
	.create({
        'headers': {
            'Authorization': jwt
        }
    })
    .get(); // 与此前使用相同,在此处调用get()或post()发起请求

关于复杂请求的预检机制导致的跨域问题

在使用了Spring Security框架的项目中,当客户端携带JWT向服务器端发起异步请求,默认会出现跨域访问的错误(即使在服务器端通过Spring MVC配置类允许跨域,此问题依然存在),可以在Spring Security的配置类中的void configure(HttpSecurity http)方法中添加配置,以解决此问题:

http.cors();

出现此问题的根本原因在于:如果客户端提交的请求自定义了请求头中的特定属性(例如配置了请求头中的Authorization属性),此请求会被视为“复杂请求”,客户端的浏览器会先向服务器端的此URL发起OPTIONS类型的请求执行“预检(PreFlight)”,如果预检不通过,则不允许提交原本尝试提交的请求。

所以,可以在Spring Security的配置类中,对所有OPTIONS类型的请求“放行”,即可解决此问题:

http.authorizeRequests() // 配置URL的访问控制

    	// 重要,以下2行代码表示:对所有OPTIONS类型的请求“放行”
        .mvcMatchers(HttpMethod.OPTIONS, "/**")
        .permitAll()

        .mvcMatchers(urls)
        .permitAll()
        .anyRequest()
        .authenticated();

或者,通过http.cors();也可以解决此问题,此方法的本质是启用了Spring Security自带的CorsFilter过滤器,此过滤器会对OPTIONS请求放行。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/383471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

华为机试题:HJ97 记负均正(python)

文章目录&#xff08;1&#xff09;题目描述&#xff08;2&#xff09;Python3实现&#xff08;3&#xff09;知识点详解1、input()&#xff1a;获取控制台&#xff08;任意形式&#xff09;的输入。输出均为字符串类型。1.1、input() 与 list(input()) 的区别、及其相互转换方…

MySQL表的增删查改(进阶)

所有操作&#xff1a;主要讲了数据库的约束&#xff0c;表之间的关系&#xff0c;新增&#xff0c;聚合查询&#xff0c;联合查询等内容。是一篇博客所有操作的记录。 844d186 风夏/mysql_learning - Gitee.com数据库约束1.1 约束条件not null -指定某个列不能储存null值。un…

17、二维图形绘制

目录 &#xff08;一&#xff09;plot绘图指令 &#xff08;1&#xff09;plot(x,y) &#xff08;2&#xff09;plot(y)。 &#xff08;3&#xff09;plot(z)。 &#xff08;4&#xff09;plot(A)。 &#xff08;5&#xff09;plot(x,A)。 &#xff08;6&#xff09;plo…

【C++】纯虚函数、纯虚析构

纯虚函数语法&#xff1a;virtual 返回值类型 函数名(参数列表) 0纯虚函数的作用&#xff1a;不用定义&#xff01;在多态中&#xff0c;通常父类中虚函数的实现是无意义的&#xff08;因为主要用子类重写的&#xff0c;父类只是为了派生子类当做一个类族的顶层出现&#xff0…

如何才能监控查看出注册表更改情况,本地组策略设置更改了哪些注册表对应值?

环境: Win11 专业版 HP480G7 Windows Sysinternals Suite 问题描述: 如何才能监控查看出注册表更改情况,本地组策略设置更改了哪些注册表对应值? 解决方案: 1.下载Windows Sysinternals Suite,解压找到ProcessMonitor 打开 2.先按ctrl+e capture 进行捕获监控 …

MRI结构像自定义脑部ROI-基于FSL

FSLmaths创建ROI 1.软件准备 在linux下安装好FLS&#xff0c;安装教程见下方视频。命令行输入FSL打开 选择FSLeyes 2.准备模板 从标准库目录里选择一个想创建的ROI的模板&#xff0c;打开看一下 我这里选择MNI152_2mm的模板&#xff0c;因为1mm的电脑会内存溢出 3.准备坐标点…

【剑指offer】JZ7 重建二叉树、JZ9 用两个栈实现队列

\描述&#xff1a; 给定节点数为 n 的二叉树的前序遍历和中序遍历结果&#xff0c;请重建出该二叉树并返回它的头结点。 例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}&#xff0c;则重建出如下图所示。 思路&#xff1a; 题上给了我们前序遍历(根 …

信息发布小程序【源码好优多】

简介 信息发布小程序&#xff0c;实现数据与小程序数据同步共享&#xff0c;通过简单的配置就能搭建自己的小程序。&#xff0c;基于微信小程序开发的小程序。 这个框架比较简单就是用微信原生开发技术进行实现的&#xff0c;可以用于信息展示等相关信息。其中目前APP比较多&am…

瓜子大王稳住基本盘,洽洽食品做对了什么?

2月24日&#xff0c;洽洽食品披露2022年业绩快报,公司预计实现营收总收入68.82亿元&#xff0c;同比增长14.98%, 实现归母净利润9.77 亿元&#xff0c;同比增长5.21%&#xff0c;业绩基本符合市场预期。来源&#xff1a;洽洽食品2022年度业绩快报2022年&#xff0c;瓜子大王洽洽…

d3 tree 实现双向动画树总结

使用d3.js 实现双向tree&#xff0c;并实现节点展开收起动画。 使用svg 绘制。 效果图 d3 d3可与快速选择批量的节点。类似jquery一样可选择元素并更改其属性值。 选择节点并设置属性 import * as D3 from d3; let svg D3.select(.tree).attr("width", 800).att…

线程池中shutdown()和shutdownNow()方法的区别

线程池中shutdown()和shutdownNow()方法的区别 一般情况下&#xff0c;当我们频繁的使用线程的时候&#xff0c;为了节约资源快速响应需求&#xff0c;我们都会考虑使用线程池&#xff0c;线程池使用完毕都会想着关闭&#xff0c;关闭的时候一般情况下会用到shutdown和shutdow…

UVa 817 According to Bartjens 数字表达式 DFS ID 迭代加深搜 逆波兰表达式

题目链接&#xff1a;According to Bartjens 题目描述&#xff1a; 给定一个由数字和一个组成的字符串&#xff0c;你需要在数字之间添加,−,∗,-,*,−,∗三种符号&#xff0c;在保证表达式合法的情况下&#xff08;同时形成的新的数字不能有前导零&#xff09;&#xff0c;使表…

java工具jconsole/jstat学习

参考视频【java】jvm指令与工具jstat/jstack/jmap/jconsole/jps/visualVM_哔哩哔哩_bilibili 一、jps 我们再windows和linux都可以看到哪些java进程。 有小伙伴又会问了 这个类是java的 那其他的这么多进程18096 /8685 这些是啥啊 其实也是java进程&#xff0c;只不过是其他程…

23.3.1调研

上一篇调研之后&#xff0c;还是没有思路&#xff0c;继续调研文献。 文章目录WEAKLY SUPERVISED EXPLAINABLE PHRASALREASONING WITH NEURAL FUZZY LOGIC模型结构ASK ME ANYTHING: A SIMPLE STRATEGY FOR PROMPTING LANGUAGE MODELSHumanly Certifying Superhuman Classifiers…

即时通讯和实时通讯的区别

即时通讯&#xff08;IM&#xff09;和实时通讯是一套网络通讯系统&#xff0c;其本质都是对信息进行转发。最大的不同点是对信息传递的时间规定。二者的区别可以从以下几个方面&#xff1a;一、场景常见的即时通讯 场景包括文字聊天、语音消息发送、文件传输、音视频播放等。通…

【RocketMQ】消息的刷盘机制

刷盘策略 CommitLog的asyncPutMessage方法中可以看到在写入消息之后&#xff0c;调用了submitFlushRequest方法执行刷盘策略&#xff1a; public class CommitLog {public CompletableFuture<PutMessageResult> asyncPutMessage(final MessageExtBrokerInner msg) {// …

docker(一)简介、环境搭建

文章目录前言一、docker简介1.什么是docker2. 什么是容器3.传统的虚拟化技术和容器之间的差别4.容器是如何运行的二、docker环境部署及测试1.环境部署&#xff1a;2.通过镜像运行容器3.拉取镜像前言 一、docker简介 1.什么是docker Docker是一个开源的应用容器引擎&#xff0…

TDEngine集群监控组件安装配置(Telegra+Grafana方案)

Tdengine的监控指标包括以下几个方面&#xff1a; 系统指标&#xff1a;CPU使用率、内存使用率、磁盘空间、网络流量等。数据库指标&#xff1a;连接数、查询数、写入数、读取数等。SQL指标&#xff1a;执行时间、执行计划、索引使用情况等。集群指标&#xff1a;节点状态、数…

生命周期:Vue,微信小程序

目录 一、vue2生命周期&#xff08;钩子函数&#xff09; 二、vue3生命周期&#xff08;钩子函数&#xff09; 三、vue-router3钩子函数&#xff08;与vue2匹配&#xff09; 1、全局钩子&#xff08;全局守卫&#xff09; 2、路由内钩子&#xff08;路由独享的守卫&#x…

Python3-字典

Python3 字典 字典是另一种可变容器模型&#xff0c;且可存储任意类型对象。 字典的每个键值 key>value 对用冒号 : 分割&#xff0c;每个对之间用逗号(,)分割&#xff0c;整个字典包括在花括号 {} 中 ,格式如下所示&#xff1a; d {key1 : value1, key2 : value2, key3…