Spring Security——结合JWT实现令牌的验证与授权

2024-06-16 14:36

本文主要是介绍Spring Security——结合JWT实现令牌的验证与授权,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

JWT(JSON Web Token)

项目总结

新建一个SpringBoot项目

pom.xml

PayloadDto

JwtUtil工具类

MyAuthenticationSuccessHandler(验证成功处理器)

JwtAuthenticationFilter(自定义token过滤器)

WebSecurityConfig配置类

WebSecurityController控制器

 index.html

项目测试


  • 本文以Spring Security项目为基础,此处只给出有关JWT的核心代码,其他代码可查看文章:http://t.csdnimg.cn/hLyKB
  • 参考文章:JWT 基础概念详解 | JavaGuide

JWT(JSON Web Token)

  • 基于token的认证方式相较于Session认证方式的好处:
    • 可以节省服务器端的开销,因为每个认证用户占一个Session对象是需要消耗服务器内存资源的,而token是在每次请求时传递给服务器端的
    • 当服务器端做集群部署时,基于token的认证方式也更容易扩展
    • 无须考虑CSRF,由于不同依赖cookie,所以采用token认证方式不会发生CSRF
    • 更适合于移动端,当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会更简单
  • JWT就是token的一种实现方式
  • JWT可以用于认证和信息交换,流程:
    • 在Web应用程序中,当用户成功登录后,服务器可以生成一个JWT并将其发送回客户端
    • 客户端存储该令牌,并在后续每次请求服务器接口时,都在请求报头中携带JWT
    • 服务器校验JWT签名,得到用户信息,如果验证通过,则根据授权规则返回前端请求的数据
  • JWT是一种用于在网络上安全传输信息的开放标准(RFC 7519),通过使用数字签名(或加密)来验证消息的发送者以及确保消息在传输过程中没有被篡改
  • JWT由三部分组成(用点号(.)分隔开)
    • Header(头部):是一个描述JWT元数据的JSON对象
      • 令牌的类型(JWT)
      • 正在使用的签名算法(如HMAC SHA256或者RSA)
    • Payload(载荷):是一个JSON对象,存放有效信息
      • 三种声明类型,关于用户和附加数据的陈述
      • 1、registered
        • iss(issuer):签发人
        • sub(subject):主题
        • aud(audience):受众
        • exp(expiration time):过期时间
        • nbf(not before):生效时间
        • iat(issued at):签发时间
        • jti(jwt id):JWT的唯一身份标识 
      • 2、public
      • 3、private
    • Signature(签名):对头部和载荷采用单向散列算法生成一个哈希码,以确保数据不会被篡改。签名用于验证消息在此过程中是否被更改,并且对于使用私钥签名的令牌还可以验证JWT的发送者是不是它所声称的
    • JWT=Header.Payload.Signature
  • 优点:
    • 简洁:由于其信息是以JSON对象的形式存储的,因此JWT是非常紧凑的
    • 自包含:JWT包含了所有必要的信息,因此不需要查询数据库来验证令牌
    • 可扩展性:由于其灵活的结构,JWT可以包含任意的额外信息
  • 缺点:
    • 信息是以Base64编码的形式存储的,所以不适合存储敏感信息,例如密码
    • 使用时要小心JWT被盗用或篡改
  • 应用场景:
    • 授权:一旦用户登录,每个后续请求就都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录(Single Sign On)是目前被广泛使用JWT的一个功能,因为它开销小,并且能够轻松地跨不同的域
    • 信息交换:可以在各方之间安全传输信息。因为可以对JWT进行签名(例如使用公钥或私钥对),所以可以确定发送者是特定的人。此外,由于签名是使用标头和有效载荷计算的,因此还可以验证内容是否被篡改

项目总结

  • 工作流程:

  • 1、用户注册和登录:

    • 用户通过注册接口提交注册信息,系统保存用户信息到数据库。
    • 用户通过登录接口提交用户名和密码,系统验证用户信息,成功后生成并返回JWT令牌。
  • 2、请求保护资源:

    • 客户端在每次请求时将JWT令牌放在请求头中。
    • JwtAuthenticationFilter 拦截请求,从请求头中提取JWT令牌,并通过 getAuthentication(token) 方法验证和解析令牌。
    • 若验证成功,将 UsernamePasswordAuthenticationToken 设置到 SecurityContextHolder 中,使当前线程拥有安全上下文。
    • 后续处理链和控制器可以通过安全上下文获取用户信息进行权限验证。
  • 3、安全考量:

    • JWT加密:使用HMAC算法对JWT进行签名,确保令牌的完整性和真实性。
    • 过期时间:设置JWT的有效期,减小令牌被盗用的风险。
    • 角色和权限:通过角色和权限控制资源访问,确保不同用户只能访问其权限范围内的资源。

新建一个SpringBoot项目

 项目的结构

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.study</groupId><artifactId>jwt</artifactId><version>0.0.1-SNAPSHOT</version><name>jwt</name><description>Demo project for Spring Boot</description><properties><java.version>8</java.version></properties><dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!--hutool是一个Java工具类库--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.5.7</version></dependency><!--jwt相关依赖--><dependency><groupId>com.nimbusds</groupId><artifactId>nimbus-jose-jwt</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

PayloadDto

package com.study.jwt.dto;import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;import java.util.List;/*** 用于封装JWT中的有效载荷*/
@Data
//这个注解单独应用时会生成 equals 和 hashCode 方法,callSuper = false 表示生成的方法不会调用父类的 equals 和 hashCode 方法
@EqualsAndHashCode(callSuper = false)
//用于生成 Builder 模式相关的代码。它会生成一个静态内部类 ExampleBuilder,使得创建对象更加简洁和灵活
@Builder
public class PayloadDto {private String sub;//主题private Long iat;//签发时间private Long exp;//过期时间private String jti;//JWT的IDprivate String username;//用户名称private List<String> authorities;//用户拥有的权限
}

JwtUtil工具类

package com.study.jwt.util;import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.study.jwt.dto.PayloadDto;import java.text.ParseException;
import java.util.Date;/*** 工具类,用于提供生成和验证JWT的方法*/
public class JwtUtil {//默认密钥public static final String DEFAULT_SECRET="mySecret";/*** 使用HMAC SHA-256签名算法* @param payloadStr 有效载荷* @param secret 密钥* @return JWS串*/public static String generateTokenByHMAC(String payloadStr,String secret) throws JOSEException {//创建JWS头,设置签名算法和类型JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT).build();//将载荷信息封装到Payload中Payload payload = new Payload(payloadStr);//创建JWS对象JWSObject jwsObject = new JWSObject(jwsHeader, payload);//创建HMAC签名器JWSSigner jwsSigner = new MACSigner(secret);//签名jwsObject.sign(jwsSigner);return jwsObject.serialize();}/*** 验证签名,提取有效载荷,以PayloadDto对象形式返回* @param token JWS串* @param secret 密钥* @return PayloadDto对象*/public static PayloadDto verifyTokenByHMAC(String token,String secret) throws ParseException, JOSEException {//从token中解析JWS对象JWSObject jwsObject = JWSObject.parse(token);//创建HMAC验证器JWSVerifier jwsVerifier = new MACVerifier(secret);if (!jwsObject.verify(jwsVerifier)) {throw new JOSEException("token签名不合法!");}String payload = jwsObject.getPayload().toString();PayloadDto payloadDto = JSONUtil.toBean(payload, PayloadDto.class);if (payloadDto.getExp() < new Date().getTime()){throw new JOSEException("token已过期!");}return payloadDto;}
}

MyAuthenticationSuccessHandler(验证成功处理器)

package com.study.jwt.config.handler;import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.JOSEException;
import com.study.jwt.dto.PayloadDto;
import com.study.jwt.util.JwtUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;/*** 修改验证成功处理器,用户成功登录后,在响应报头中发送token*/
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {// 获取认证主体信息Object principal = authentication.getPrincipal();// 用户登录成功后,在响应头中发送 tokenif(principal instanceof UserDetails){// 如果主体信息是用户详情实例UserDetails user=(UserDetails)principal;// 获取用户的权限信息Collection<? extends GrantedAuthority> authorities=authentication.getAuthorities();List<String> authoritiesList = new ArrayList<>(authorities.size());// 将用户权限列表转换为字符串列表authorities.forEach(authority -> {authoritiesList.add(authority.getAuthority());});// 定义 token 的创建时间和过期时间Date now = new Date();DateTime exp = DateUtil.offsetSecond(now, 60 * 60); // 1小时后过期// 创建 payload 数据传输对象PayloadDto payloadDto=PayloadDto.builder().sub(user.getUsername()).iat(now.getTime()).exp(exp.getTime()).jti(UUID.randomUUID().toString()).username(user.getUsername()).authorities(authoritiesList).build();String token=null;try {// 生成 JWT tokentoken= JwtUtil.generateTokenByHMAC(//nimbus-jose-jwt所使用的HMAC SHA256算法所需密钥长度为256位(32字节),因此先用md5加密JSONUtil.toJsonStr(payloadDto),SecureUtil.md5(JwtUtil.DEFAULT_SECRET));// 设置响应头和响应内容类型response.setHeader("Authorization",token);response.setContentType("application/json;charset=UTF-8");PrintWriter out = response.getWriter();out.write("登录成功!");out.close();} catch (JOSEException e) {e.printStackTrace();}}}
}

JwtAuthenticationFilter(自定义token过滤器)

package com.study.jwt.filter;import cn.hutool.crypto.SecureUtil;
import com.nimbusds.jose.JOSEException;
import com.study.jwt.dto.PayloadDto;
import com.study.jwt.util.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** JwtAuthenticationFilter是一个过滤器,拦截用户请求,验证token* 继承OncePerRequestFilter,保证在任何servlet容器上每次请求调度都能执行一次。*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {/*** 过滤器逻辑,提取和验证JWT,如果有效则设置认证信息。** @param request     HttpServletRequest对象* @param response    HttpServletResponse对象* @param filterChain FilterChain对象*/@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("Authorization"); // 从请求头中获取JWTif (token == null) {// 如果请求头中没有token,则直接放行filterChain.doFilter(request, response);return;}// 如果请求头中有token,则进行解析,并且设置认证信息try {// 验证并解析token,并将认证信息设置到SecurityContext中//它将经过认证的用户信息设置到当前的安全上下文中 (SecurityContext),从而使得后续的安全决策可以基于该用户的信息进行SecurityContextHolder.getContext().setAuthentication(getAuthentication(token));filterChain.doFilter(request, response); // 继续执行过滤链} catch (ParseException | JOSEException e) {e.printStackTrace(); // 打印异常信息}}/*** 验证token,并解析token,返回以用户名和密码所表示的经过身份验证的主体的令牌。** @param token JWT字符串* @return UsernamePasswordAuthenticationToken对象* @throws ParseException JSON解析异常* @throws JOSEException  JWT处理异常*/private UsernamePasswordAuthenticationToken getAuthentication(String token) throws ParseException, JOSEException {// 使用HMAC算法验证并解析JWT,获取PayloadDto对象PayloadDto payloadDto = JwtUtil.verifyTokenByHMAC(token, SecureUtil.md5(JwtUtil.DEFAULT_SECRET));String username = payloadDto.getUsername(); // 从Payload中获取用户名List<String> roles = payloadDto.getAuthorities(); // 从Payload中获取角色列表Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();// 将角色列表转换为Spring Security的SimpleGrantedAuthority集合roles.forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));if (username != null) {return new UsernamePasswordAuthenticationToken(username, null, authorities);}return null; // 如果用户名为空,返回null}
}

WebSecurityConfig配置类

package com.study.jwt.config;import com.study.jwt.filter.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@EnableWebSecurity // 启用Spring Security的Web安全支持
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler; // 成功处理器注入@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler; // 失败处理器注入@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;/*** 配置HTTP安全** @param http HttpSecurity对象* @throws Exception 异常处理*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/", "/home", "/login").permitAll() // 允许所有人访问.antMatchers("/admin/**").hasRole("ADMIN") // 只有具有 "ADMIN" 角色的用户才能访问以 "/admin/" 开头的路径.anyRequest().hasRole("USER") // 其他请求必须有USER角色.and().formLogin().loginPage("/login") // 指定登录页面.successHandler(authenticationSuccessHandler) // 登录成功处理器.failureHandler(authenticationFailureHandler) // 登录失败处理器.and().logout() // 启用注销功能.and()// 添加自定义的 JWT 过滤器.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);http.csrf().disable(); // 禁用CSRF保护}/*** 配置密码编码器*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); // 使用BCrypt加密密码}
}

WebSecurityController控制器

package com.study.jwt.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;/*** 1.将configure(HttpSecurity http)方法中设置的不同的URL映射到不同的页面* 2.方法返回的是视图名称,需要视图解析器将视图名称解析成实际的HTML文件* 然后访问url就可以跳转到HTML页面了,否则返回的只是一个字符串* 3.在application.properties配置文件中配置视图解析器,springboot已经默认配置好了,你不用写了*/
@Controller
public class WebSecurityController {/*** 登录后跳转到home.html页面*/@GetMapping("/home")public String home(){return "home";}/*** 登录页面*/@GetMapping("/login")public String login(){return "login";//login.html}/*** 当访问/resource时,会重定向到/login,登录后才可以访问受保护的页面resource.html*/@GetMapping("/resource")public String resource(){return "resource";//resource.html}@GetMapping("/admin/index")public String index(){return "index";//index.html}
}

 index.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>index</title>
</head>
<body>
这是网站的首页!
</body>
</html>

项目测试

1、访问网址:http://localhost:8080/login

下面的Headers里的Authorization里的Value即为token,它被点号(.)分成三个部分,第一部分为Header,第二部分为Payload,第三部分为Signature

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwiZXhwIjoxNzE4NDM4NDY1NDM3LCJpYXQiOjE3MTg0MzQ4NjU0MzcsImp0aSI6ImQyNDFmMDlkLTgwNzYtNDZhZC1hYTg0LTc3MjQxNTIyZjUyZSIsInVzZXJuYW1lIjoiYWRtaW4ifQ.izNsanv4m-UJiIH23m8A2FvqWIvMfy0AEbleanxnMt8

2、访问网址:http://localhost:8080/resource

携带token进行后续访问:把登录时的token复制到Headers里的新增的Authorization里,访问受保护资源 

3、访问网址:http://localhost:8080/admin/index

该地址需要角色为“ADMIN”,所以需要使用第二个账号"admin"登录

如果不用特定账号登录,则返回403错误

这篇关于Spring Security——结合JWT实现令牌的验证与授权的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1066713

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

Security OAuth2 单点登录流程

单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性。当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录。这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中。相同的,单一注销(single sign-off)就是指

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu