jsd2205-csmall-passport(Day13)
1. 解析JWT时可能出现的错误
如果使用过期的JWT,在解析时将出现错误:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-09-06T17:33:03Z. Current time: 2022-09-08T09:04:26Z, a difference of 142283930 milliseconds. Allowed clock skew: 0 milliseconds.
如果使用的JWT数据的签名有误,在解析时将出现错误:
io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
如果使用的JWT数据格式有误,在解析时将出现错误:
io.jsonwebtoken.MalformedJwtException: Unable to read JSON value: {"alg !L��؈�������)]P�
后续,将需要对这3种异常进行捕获并处理!
2. 使用JWT实现认证
在使用Spring Security框架处理认证时,如果认证通过,必须把通过认证的用户的信息存入到SecurityContext
(Spring Security框架的上下文)对象中,后续,Spring Security框架会自动的尝试从SecurityContext
中获取认证信息,如果获取到有效的认证信息,则视为“已登录”,否则,将视为“未登录”!
使用JWT实现认证需要完成的开发任务:
- 当认证通过时生成JWT,并将JWT响应到客户端
- 当客户端后续提交请求时,应该自觉携带JWT,而服务器端将对JWT进行解析,如果解析成功,将得此客户端的用户信息,并将认证信息存入到
SecurityContext
中
3. 当认证通过时生成JWT,并将JWT响应到客户端
首先,需要修改IAdminService
中处理认证的方法(login()
方法)的声明,将返回值类型修改为String
:
String login(AdminLoginInfoDTO adminLoginInfoDTO);
并且,AdminServiceImpl
中方法的声明也同步修改,在实现过程中,当通过认证后,应该生成JWT并返回:
@Override
public String login(AdminLoginInfoDTO adminLoginInfoDTO) {log.debug("开始处理【登录认证】的业务,参数:{}", adminLoginInfoDTO);// 调用AuthenticationManager的authenticate()方法执行认证// 在authenticate()方法的执行过程中// Spring Security会自动调用UserDetailsService对象的loadUserByUsername()获取用户详情// 并根据loadUserByUsername()返回的用户详情自动验证是否启用、判断密码是否正确等Authentication authentication= new UsernamePasswordAuthenticationToken(adminLoginInfoDTO.getUsername(),adminLoginInfoDTO.getPassword());Authentication authenticateResult= authenticationManager.authenticate(authentication);log.debug("Spring Security已经完成认证,且认证通过,返回的结果:{}", authenticateResult);log.debug("返回认证信息中的当事人(Principal)类型:{}", authenticateResult.getPrincipal().getClass().getName());log.debug("返回认证信息中的当事人(Principal)数据:{}", authenticateResult.getPrincipal());// 从认证返回结果中取出当事人信息User principal = (User) authenticateResult.getPrincipal();String username = principal.getUsername();log.debug("认证信息中的用户名:{}", username);// 生成JWT,并返回// 准备Claims值Map<String, Object> claims = new HashMap<>();claims.put("username", username);// JWT的过期时间Date expiration = new Date(System.currentTimeMillis() + 15 * 24 * 60 * 60 * 1000);log.debug("即将生成JWT数据,过期时间:{}", expiration);// JWT的组成:Header(头:算法和Token类型)、Payload(载荷)、Signature(签名)String secretKey = "97iuFDVDfv97iuk534Tht3KJR89kBGFSBgfds";String jwt = Jwts.builder()// Header.setHeaderParam("alg", "HS256").setHeaderParam("typ", "JWT")// Payload.setClaims(claims).setExpiration(expiration)// Signature.signWith(SignatureAlgorithm.HS256, secretKey).compact();log.debug("已经生成JWT数据:{}", jwt);return jwt;
}
提示:以上生成JWT的代码暂未封装!
最后,在AdminController
中,将处理认证的方法(login()
方法)的返回值类型由JsonResult<Void>
修改为JsonResult<String>
,并且,在方法体中,调用IAdminService
的认证方法时,必须获取返回值,最终将此返回值封装到JsonResult
对象中,响应到客户端:
// http://localhost:9081/admins/login
@ApiOperation("管理员管理")
@ApiOperationSupport(order = 88)
@PostMapping("/login")
public JsonResult<String> login(AdminLoginInfoDTO adminLoginInfoDTO) {String jwt = adminService.login(adminLoginInfoDTO);return JsonResult.ok(jwt);
}
4. 解析JWT并处理SecurityContext
当客户端成功的通过认证后,将可以得到JWT,后续,客户端可以携带JWT提交请求,但是,作为服务器端,并不知道客户端将会向哪个URL提交请求,或者说,不管客户端向哪个URL提交请求,服务器端都应该尝试解析JWT,以识别客户端的身份,则解析JWT的代码可以使用“过滤器”组件来实现!
过滤器(
Filter
):是Java EE中的核心组件,此组件是最早接收到请求的组件!并且,此组件可作用于若干个请求的处理过程。
关于客户端携带JWT,业内通用的做法是:将JWT携带在请求头(Request Header)中名为Authorization
的属性中!
所以,此过滤器将固定的通过请求头(Request Header)中的Authorization
属性获取JWT数据,并尝试解析。
由于Spring Security框架判断是否登录的标准是:在SecurityContext
中是否存在认证信息!所以,当成功解析JWT数据后,应该将认证信息保存到SecurityContext
中。
另外,还有几个细节:
- 一旦
SecurityContext
中存在认证信息,在后续的访问中,即使不携带JWT数据,只要在SecurityContext
还存在此前存入的认证信息,就会被视为“已经通过认证”,所以,为了避免此问题,应该在接收到请求的那一刻就直接清除SecurityContext
- 认证的过程应该是“先将认证信息存入到
SecurityContext
(由我们的过滤器执行),再判断是否是通过认证的状态(由Spring Security的过滤器等组件执行)”,所以,当前过滤器必须在Spring Security的相关过滤器之前执行。
所以,在根包下创建filter.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.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;/*** 解析JWT的过滤器** 1. 首先,清除SecurityContext中的认证信息* 2. 如果客户端没有携带JWT,则放行,由后续的组件进行处理* 3. 如果客户端携带了有效的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 {log.debug("处理JWT的过滤器开始执行……");// 清除SecurityContext中原有的认证信息// 避免曾经成功访问过,后续不携带JWT也能被视为“已认证”SecurityContextHolder.clearContext();// 尝试从请求头中获取JWT数据String jwt = request.getHeader("Authorization");log.debug("尝试从请求头中获取JWT数据:{}", jwt);// 判断客户端是否携带了有效的JWT数据,如果没有,直接放行if (!StringUtils.hasText(jwt) || jwt.length() < 113) {log.debug("获取到的JWT被视为【无效】,过滤器执行【放行】");filterChain.doFilter(request, response);return;}// 程序执行到此处,表示客户端携带了有效的JWT,则尝试解析log.debug("获取到的JWT被视为【有效】,则尝试解析……");String secretKey = "97iuFDVDfv97iuk534Tht3KJR89kBGFSBgfds";Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();String username = claims.get("username", String.class);log.debug("从JWT中解析得到【username】的值:{}", username);// 准备权限,将封装到认证信息中List<GrantedAuthority> authorityList = new ArrayList<>();GrantedAuthority authority = new SimpleGrantedAuthority("这是一个山寨的权限");authorityList.add(authority);// 准备存入到SecurityContext的认证信息Authentication authentication= new UsernamePasswordAuthenticationToken(username, null, authorityList);// 将认证信息存入到SecurityContext中SecurityContext securityContext = SecurityContextHolder.getContext();securityContext.setAuthentication(authentication);log.debug("过滤器执行【放行】");filterChain.doFilter(request, response);}}
然后,在SecurityConfiguration
中自动装配此过滤器:
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
并在configurer()
方法中补充:
// 将JWT过滤器添加在Spring Security的UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthorizationFilter,UsernamePasswordAuthenticationFilter.class);
完成后,重启项目,在Knife4j的在线API文档中,先不携带JWT并使用正确的账号登录,然后,携带登录返回的JWT即可向那些不在白名单中的URL进行访问!
5. 关于账号的权限
当处理认证时,应该从数据库中查询出此用户的权限,并且,将权限封装到UserDetails
对象中,当认证成功后,返回的认证对象中的当事人信息就会包含权限信息,接下来,可以将权限信息也写入到JWT中!
后续,在解析JWT时,也可以从中解析得到权限信息,并将权限信息存入到SecurityContext
中,则后续Spring Security的相关组件可以实现对权限的验证!
6. 查询管理员的权限
在处理认证时,会调用AdminMapper
接口中的AdminLoginInfoVO getLoginInfoByUsername(String username);
方法,此方法的返回值应该包含管理员的权限。
则SQL语句大致是:
selectams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.value
from ams_admin
left join ams_admin_role on ams_admin.id=ams_admin_role.admin_id
left join ams_role_permission on ams_admin_role.role_id=ams_role_permission.role_id
left join ams_permission on ams_role_permission.permission_id=ams_permission.id
where username='root';
为了保证查询结果可以封装权限信息,需要在返回值类型中添加属性:
@Data
public class AdminLoginInfoVO implements Serializable {private Long id;private String username;private String password;private Integer enable;private List<String> permissions; // 新增}
然后,重新配置getLoginInfoByUsername()
方法映射的SQL查询:
<!-- AdminLoginInfoVO getLoginInfoByUsername(String usernanme); -->
<select id="getLoginInfoByUsername" resultMap="LoginResultMap">SELECT<include refid="LoginQueryFields"/>FROMams_adminLEFT JOIN ams_admin_role ON ams_admin.id=ams_admin_role.admin_idLEFT JOIN ams_role_permission ON ams_admin_role.role_id=ams_role_permission.role_idLEFT JOIN ams_permission ON ams_role_permission.permission_id=ams_permission.idWHEREusername=#{username}
</select><sql id="LoginQueryFields"><if test="true">ams_admin.id,ams_admin.username,ams_admin.password,ams_admin.enable,ams_permission.value</if>
</sql><!-- collection标签:用于配置返回结果类型中List类型的属性 -->
<!-- collection标签的ofType属性:List中的元素类型 -->
<!-- collection子级:需要配置如何创建出List中的每一个元素 -->
<resultMap id="LoginResultMap" type="cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO"><id column="id" property="id"/><result column="username" property="username"/><result column="password" property="password"/><result column="enable" property="enable"/><collection property="permissions" ofType="java.lang.String"><constructor><arg column="value"/></constructor></collection>
</resultMap>
完成后,应该及时测试!
jsd2205-csmall-passport(Day13)相关推荐
- JSD-2204-Session-Token-JWT-Day12
1.开发自定义的登录流程 目前,在passport项目中,登录是由Security框架提供的页面的表单来输入用户名.密码,且由Security框架自动处理登录流程,不适合前后端分离的开发模式!所以,需 ...
- 关于Spring Security框架 关于单点登录sso
1.Spring Security的作用 Spring Security主要解决了认证和授权相关的问题. 认证(Authenticate):验证用户身份,即登录. 授权(Authorize):允许用户 ...
- 基于Spring Security与JWT实现单点登录
基于RBAC的权限管理 RBAC(Role-Based Access Control):基于角色的访问控制 当前项目中,RBAC具体的表现为: 管理员表:ams_admin 角色表:ams_role ...
- Spring Security 框架详解
SECURITY Spring Security框架 Spring Security框架主要解决了认证与授权的相关问题. 添加依赖 在Spring Boot项目中,需要使用Spring Securit ...
- Spring Security关于用户身份认证与授权
Spring Security是用于解决认证与授权的框架. 创建spring项目,添加依赖 <!-- Spring Boot Security:处理认证与授权 --><depende ...
- My batis动态SQL
通过xml配置文件的方式,配置Mapper中需要用到的SQL语句 之前配置执行的SQL语句是通过注解的方式, 注解方式的话如果SQL语句太长存在折行时会导致SQL语句不够直观, 一些复杂的关联查询 不 ...
- php composer 无法下载,php – composer无法安装laravel / passport
我用它创建了一个新项目 laravel new blogposts 使用全局安装在我的ubuntu 18上的"Laravel Installer 2.0.1". 当我尝试使用安装护 ...
- Laravel 5.5 使用 Passport 实现 Auth 认证
最近在写一个前后端分离项目,本来想用 Jwt-auth + Dingo 开发的,但是略感笨重,于是想到了 Laravel 的 Passport 和 5.5 新出的 Api Resource.Larav ...
- php oauth2.0 实例,详解laravel passport OAuth2.0的4种模式
参考: 1... 熟悉的场景 某个网站,某用户未注册,注册时提示可微信账号登录(github, google都有类似 某网站是第三方(客户端), 认证服务器和资源服务器都在微信,资源是指微信的用户名, ...
最新文章
- 数十篇推荐系统论文被批无法复现:源码、数据集均缺失,性能难达预期
- Flex使用cookie保存登状态
- Dynamics CRM中跨域调用Web API 2
- The 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine.
- Linux crypto相关知识的汇总 Linux加密框架crypto对称算法和哈希算法加密模式
- Ext.Net 1.2.0_Ext.Net.TreePanel 勾选“纠结”发送给服务器端的方法
- Linux中的中断管理机制
- xadmin在Django 1.11中的使用及中英文切换
- 《Spring1之 第一次站立会议(重发)》
- 【学习OpenCV4】什么是图像的直方图?如何获取直方图?
- Candy leetcode java
- 在线课程培训系统源码 在线授课 在线教育源码 网课小程序源码
- 未来5年智慧城市宽带入户超百兆
- mirrorlink
- 关于升级短信源码开发接入SMPP通道
- 关于需求响应式公共交通的那些事(上)
- 2022年烷基化工艺模拟考试题及烷基化工艺模拟考试题库
- 每天一大杯可乐,会不会骨质酥松哇?
- 21点扑克游戏的出牌策略的研究
- 「行业/市场分析」简说
热门文章
- Ruoyi框架学习--Vue前端配置文件详解
- 微信表情150个限制怎么破?教你一招
- openmv 神经网络 超出内存_星瞳科技OpenMV视频教程22-神经网络cifar_10
- Spring Cloud实战(三)-监控中心
- SAP各种BOM详解(包含常用BAPI)
- java实现两个数运算_用java做简单的计算器类,实现两个数字的加减乘除运算
- 也谈把程序写好 —— 一点初级程序员的鄙见
- 光量子计算机的信息载体,如何使“孤傲”的光子改变彼此的量子态?
- 面向对象编程基本概念
- 论文那些事—ZOO: Zeroth Order Optimization Based Black-box Attacks