当前位置:网站首页>JSD-2204-Session-Token-JWT-Day12
JSD-2204-Session-Token-JWT-Day12
2022-08-08 19:37:00 【程序猿 Monkey】
1.开发自定义的登录流程
目前,在passport项目中,登录是由Security框架提供的页面的表单来输入用户名、密码,且由Security框架自动处理登录流程,不适合前后端分离的开发模式!所以,需要自行开发登录流程!
关于自定义的登录流程,主要需要:
- 在业务逻辑实现类中,调用Security的验证机制来执行登录认证
- 在控制器类中,自定义处理请求,用于接收登录请求及请求参数,并调用业务逻辑实现类来实现认证
1.1关于在Service中调用Security的认证机制:
当需要调用Security框架的认证机制时,需要使用AuthenticationManager对象,可以在Security配置类中重写authenticationManager()方法,在此方法上添加@Bean注解,由于当前类本身是配置类,所以Spring框架会自动调用此方法,并将返回的结果保存到Spring容器中:
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
在IAdminService中添加处理登录的抽象方法:
void login(AdminLoginDTO adminLoginDTO);
在AdminServiceImpl中,可以自动装配AuthenticationManager对象:
@Autowired
private AuthenticationManager authenticationManager;
并实现接口中的方法:
@Override
public void login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 调用AuthenticationManager执行认证
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
authenticationManager.authenticate(authentication);
log.debug("认证通过!");
}
1.2在控制器中接收登录请求,并调用Service:
在根包下创建pojo.dto.AdminLoginDTO类:
@Data
public class AdminLoginDTO implements Serializable {
private String username;
private String password;
}
在AdminController中添加处理请求的方法:
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult<Void> login(AdminLoginDTO adminLoginDTO) {
log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO);
adminService.login(adminLoginDTO);
return JsonResult.ok();
}
为了保证能对以上路径直接发起请求,需要将此路径(/admins/login)添加到Security配置类的“白名单”中。
完成后,启动项目,可以通过Knife4j的调试来测试登录,当登录成功时将响应正确,当用户名或密码错误时,将响应错误(需要统一处理异常)。
1.3注意:即使登录成功,也不可以实现其它请求的访问!
2.关于Session
HTTP协议本身是无状态协议,所以,无法识别用户的身份!
为了解决此问题,经编程时,引入了Session机制,用于保存用户的某些信息,可识别用户的身份!
Session的本身是在服务器端的内存中一个类似Map结构的数据,每个客户端在提交请求时,都会携带一个由服务器端首次响应时分配的Session ID,作为Map的Key,由于此Session ID具有极强的唯一性,所以,每个客户端的Session ID理论上都是不相同的,从而服务器可以识别客户端!
由于Session是保存在服务器端的内存中的,在一般使用时,并不适用于集群!
3.Token
Token:令牌,票据。
目前,推荐使用Token来保存用户的身份标识,使之可以用于集群!
相比Session ID是没有信息含义的,Token则是有信息含义的数据,当客户端向服务器端提交登录请求后,服务器商认证通过就会将此用户的信息保存在Token中,并将此Token响应到客户端,后续,客户端在每次请求时携带Token,服务器端即可识别用户的身份!
4.JWT
JWT = JSON Web Token
JWT是使用JSON格式表示一系列的数据的Token。
当需要使用JWT时,应该在项目中添加依赖:
<!-- JJWT(Java JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
然后,通过测试,实现生成JWT和解析JWT。
@Slf4j
public class JwtTests {
// 密钥(盐)
String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
// 测试生成JWT
@Test
public void testGenerateJwt() {
// 准备Claims
Map<String, Object> claims = new HashMap<>();
claims.put("id", 9527);
claims.put("name", "liulaoshi");
// JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
String jwt = Jwts.builder()
// Header:用于声明算法与此数据的类型,以下配置的属性名是固定的
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "jwt")
// Payload:用于添加自定义数据,并声明有效期
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 3 * 60 * 1000))
// Signature:用于指定算法与密钥(盐)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
log.debug("JWT = {}", jwt);
// eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9
// .
// eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzMTUyMX0
// .
// TFyWBZ3l-y6rYbEYiVBbQjqnFNsFFR07K8lDES9TPs4
// eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTM0N30.7rj8Lhus1EYXUxE4Zy1wx1WFpbvxIQEmya3-A9WZP20
// eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M
}
@Test
public void testParseJwt() {
String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJuYW1lIjoibGl1bGFvc2hpIiwiaWQiOjk1MjcsImV4cCI6MTY1OTkzOTUzMH0.lwD_PzrqGXEgQs3KmMjsYzTmhsKbGhKnd1WkDkFpj5M";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Object id = claims.get("id");
Object name = claims.get("name");
log.debug("id={}", id);
log.debug("name={}", name);
}
}
如果JWT数据已经过期,将出现错误:
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-08-08T12:05:21Z. Current time: 2022-08-08T14:11:34Z, a difference of 7573854 milliseconds. Allowed clock skew: 0 milliseconds.
如果JWT签名有误(JWT数据的最后一段出错,或生成与解析时使用的secretKey不同),将出现错误:
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|b:"HS256","typ":"jwt"}
4.1关于JWT在项目中的应用
4.1.1生成JWT
应该在用户登录时,视为”认证成功“后,生成JWT,并将此数据响应到客户端。
在业务层,调用AuthenticationManager的authenticate()方法后,得到的返回结果例如:
UsernamePasswordAuthenticationToken [
Principal=org.springframework.security.core.userdetails.User [
Username=root,
Password=[PROTECTED],
Enabled=true,
AccountNonExpired=true,
credentialsNonExpired=true,
AccountNonLocked=true,
Granted Authorities=[权限列表]
],
Credentials=[PROTECTED],
Authenticated=true,
Details=null,
Granted Authorities=[权限列表]
]
可以看到,认证返回的数据中将包含成功认证的用户信息,也是当初用于执行认证的信息(UserDetailsServiceImpl中返回的结果),可以从此认证结果中获取用户相关数据,并写入到JWT中,则需要:
- 将业务接口中的登录方法返回值类型改为
String,表示认证成功后返回的JWT - 将业务实现类中的登录方法返回值一并修改
- 在业务实现类中,当认证成功后,获取需要写入到JWT中的数据(例如:用户名等),并生成JWT,返回JWT
关于业务实现类的登录方法:
@Override
public String login(AdminLoginDTO adminLoginDTO) {
// 日志
log.debug("开始处理【管理员登录】的业务,参数:{}", adminLoginDTO);
// 调用AuthenticationManager执行认证
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(), adminLoginDTO.getPassword());
Authentication authenticateResult = authenticationManager.authenticate(authentication);
log.debug("认证通过,返回的结果:{}", authenticateResult);
log.debug("认证结果中的Principal的类型:{}",
authenticateResult.getPrincipal().getClass().getName());
// 处理认证结果
User loginUser = (User) authenticateResult.getPrincipal();
log.debug("认证结果中的用户名:{}", loginUser.getUsername());
// 生成JWT
String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
// 准备Claims
Map<String, Object> claims = new HashMap<>();
claims.put("username", loginUser.getUsername());
// JWT的组成部分:Header(头),Payload(载荷),Signature(签名)
String jwt = Jwts.builder()
// Header:用于声明算法与此数据的类型,以下配置的属性名是固定的
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "jwt")
// Payload:用于添加自定义数据,并声明有效期
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 14 * 24 * 60 * 60 * 1000))
// Signature:用于指定算法与密钥(盐)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
log.debug("生成的JWT:{}", jwt);
return jwt;
}
在控制器中,将处理登录请求的方法的返回值类型改为JsonResult<String>,并在调用业务方法时获取返回值,封装到返回的对象中:
@ApiOperation("管理员登录")
@ApiOperationSupport(order = 50)
@PostMapping("/login")
public JsonResult<String> login(AdminLoginDTO adminLoginDTO) {
log.debug("准备处理【管理员登录】的请求:{}", adminLoginDTO);
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
完成后,重启项目,在Knife4j的调试功能中,使用正常的用户名和密码发起登录请求,将响应JWT结果,例如:
{
"state": 20000,
"data": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJleHAiOjE2NjExNTIzOTUsInVzZXJuYW1lIjoic3VwZXJfYWRtaW4ifQ.rFACBsBY8w8oNpR80n2YiplsEUIqw5bnCIsC5UAqsww"
}
4.2在服务器端检查并解析JWT
经过以上登录认证并响应JWT后,客户端在后续发起请求时,应该自主携带JWT数据,而服务器端应该尝试检查并解析JWT。
由于客户端在发起多种不同请求时都应该携带JWT,且服务器端都应该检查并尝试解析,所以,服务器端检查并解析的过程,应该发生在比较”通用“的组件中,即无论客户端提交的是哪个路径的请求,这个组件都应该执行!通常,会使用”过滤器“组件进行处理。
在项目的根包下创建filter.JwtAuthrozationFilter类,继承自OncePerRequestFilter,并在此类上添加@Component注解:
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
public JwtAuthorizationFilter() {
log.debug("创建过滤器:JwtAuthorizationFilter");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.debug("执行JwtAuthorizationFilter");
// 过滤器链继续执行,相当于:放行
filterChain.doFilter(request, response);
}
}
关于客户端提交请求时携带JWT数据,业内通用的做法是在请求头中添加Authorization属性,其值就是JWT数据,所以,服务器端获取JWT的做法应该是:从请求头中的Authorization属性中获取JWT数据!
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.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;
@Slf4j
@Component
public class JwtAuthorizationFilter extends OncePerRequestFilter {
public JwtAuthorizationFilter() {
log.debug("创建过滤器:JwtAuthorizationFilter");
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
log.debug("执行JwtAuthorizationFilter");
// 从请求头中获取JWT
String jwt = request.getHeader("Authorization");
log.debug("从请求头中获取JWT:{}", jwt);
// 判断JWT数据是否不存在
if (!StringUtils.hasText(jwt) || jwt.length() < 80) {
log.debug("获取到的JWT是无效的,直接放行,交由后续的组件继续处理!");
// 过滤器链继续执行,相当于:放行
filterChain.doFilter(request, response);
// 返回,终止当前方法本次执行
return;
}
// 尝试解析JWT
String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
Object username = claims.get("username");
log.debug("从JWT中解析得到username:{}", username);
// 准备Authentication对象,后续会将此对象封装到Security的上下文中
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("临时使用的权限"));
Authentication authentication = new UsernamePasswordAuthenticationToken(
username, null, authorities);
// 将用户信息封装到Security的上下文中
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
log.debug("已经向Security的上下文中写入:{}", authentication);
// 过滤器链继续执行,相当于:放行
filterChain.doFilter(request, response);
}
}
完成后,还需要将此过滤器添加在Security框架的UsernamePasswordAuthenticationFilter过滤器之前,需要在Security配置类中,先自动装配自定义的过滤器对象:
@Autowired
private JwtAuthorizationFilter jwtAuthorizationFilter;
然后,在configurer(HttpSecurity http)方法中添加:
// 将“JWT过滤器”添加在“认证过滤器”之前
http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
最后,在JWT过滤器执行之初,先清除Security上下文中的数据,以避免”一旦提交JWT将认证对象存入到Security上下文中,后续不携带JWT也能访问“的问题:
// 清除Security上下文中的数据
SecurityContextHolder.clearContext();
完成后,启动项目,在Knife4j的调试功能中,携带JWT可以发起任何需要登录才能访问的请求,反之,这些请求不携带JWT将不允许访问。
边栏推荐
猜你喜欢
The difference between Redis' memory elimination strategy and expired deletion strategy

生成验证码工具类

分布式文件系统fastDFS

What is the main purpose of software testing?

虚假信息处理最新有何进展?KDD2022《打击错误信息和应对媒体偏见》教程,161页ppt

电脑win键没有反应(最全方案)

golang for循环详解

What are the three main aspects of digital factory construction?

PX4-Things you need to know for secondary development of flight control-Cxm

音视频技术开发周刊 | 257
随机推荐
Salesforce开发之 apex操作批准过程(Approval Process)
黑猫带你学Makefile第7篇:Makefile简单例程一览
BP neural network
IJCAI 2022 | Can Graph Neural Networks Detect Anomalies?
梅科尔工作室OpenHarmony设备开发培训笔记-第六章学习笔记
无标题文章
信号与系统【x(t)*h(t)=y(t) 求h(t)】附matlab代码
C语言初阶-结构体
挖财学堂帮开通的证券账户是真的吗?安全吗
网络工程师怎么系统性学习?这份网工资料包帮你解决
Canvans:绘制饼图和玫瑰饼图
进化的黑产 vs 进击的蚂蚁:支付宝的每一次点击,都离不开一张“图”的守护
最长子串(长沙理工大学第十一届程序设计竞赛 离线 做了n天.....崩溃了)
黑猫带你学Makefile第4篇:Makefile中变量的使用
室外光纤资源管理——可视化管理平台
从 VLAN 到 IPVLAN: 聊聊虚拟网络设备及其在云原生中的应用
nyoj 712 探 寻 宝 藏(双线dp 第六届河南省程序设计大赛)
Flutter Chart
黑猫带你学Makefile第8篇:uboot/kernel中的makefile基本语法与流程
聚名十周年线上庆典正式开启,发送祝福即有好礼相赠~