SpringBoot2.3集成Spring Security(二) JWT认证

news/2024/10/22 15:35:25/

项目背景

紧接上文,我们已经完成了 SpringBoot中集成Spring Security,并且用户名帐号和密码都是从数据库中获取。但是这种方式还是不能满足现在的开发需求。

使用JWT的好处:

  1. 无状态认证:JWT本身包含了认证信息和声明,服务器不需要在会话中保存任何状态。这样使得应用程序可以更加容易的扩展,并且更适合分布式部署和微服务架构。
  2. 跨域支持:由于JWT在HTTP头部中进行传输,因此它可以轻松的支持跨域请求。
  3. 灵活性:JWT可以包含任意数量的声明,这些声明可以用来传递用户、角色、或者其他相关的元数据。这些数据可以在服务器端和客户端之间共享,从而简化了授权和访问控制管理。
  4. 安全性:JWT使用数字签名或者加密算法来验证其完整性和真实性。这确保了JWT在传输过程中不会被篡改或伪造。

JWT(Json Web Tokens)

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDS

翻译:JSON Web Token (JWT) 是一个开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。 此信息可以验证和信任,因为它是数字签名的。 JWT 可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。

JWT组成

  • header: 存放签名的生成算法。
  • payload:存放用户名、token的生成时间和过期时间。
  • signature:以header和payload生成的签名,一旦header和payload被篡改,验证将失败。
    可以在该网站上进行解析:https://jwt.io/
    在这里插入图片描述

Spring Security集成JWT

maven引入

 		<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>

首先不论是不是Spring Security中集成JWT,我们得先有个工具类。这个工具类的主要内容是什么呢?
创建JWT、验证JWT、 解析JWT

步骤一:

JwtUtils工具类

/*** jwt工具类** @author caojing* @since 2023/6/14*/
public class JwtUtils {/*** token过期时间*/public static final long EXPIRE = 1000 * 60 * 60 * 24;/*** 秘钥*/public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";/*** 生成token字符串的方法** @param id* @param nickname* @return*/public static String getJwtToken(String id, String nickname) {String jwtToken = Jwts.builder().setHeaderParam("typ", "JWT").setHeaderParam("alg", "HS256").setSubject("guli-user").setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + EXPIRE))//设置token主体部分 ,存储用户信息.claim("id", id).claim("nickname", nickname).signWith(SignatureAlgorithm.HS256, APP_SECRET).compact();return jwtToken;}/*** 判断token是否存在与有效** @param jwtToken* @return*/public static boolean checkToken(String jwtToken) {if (StringUtils.isEmpty(jwtToken)) {return false;}try {Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 判断token是否存在与有效** @param request* @return*/public static boolean checkToken(HttpServletRequest request) {try {String jwtToken = request.getHeader("Authorization");if (StringUtils.isEmpty(jwtToken)) {return false;}Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);} catch (Exception e) {e.printStackTrace();return false;}return true;}/*** 根据token字符串获取会员id** @param request* @return*/public static String getUserIdByJwtToken(HttpServletRequest request) {String jwtToken = request.getHeader("Authorization");if (StringUtils.isEmpty(jwtToken)) {return "";}Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);Claims claims = claimsJws.getBody();return (String) claims.get("id");}/*** 验证jwt*/public static Claims verifyJwt(String token) {Claims claims;try {//得到DefaultJwtParserclaims = Jwts.parser()//设置签名的秘钥.setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();} catch (Exception e) {e.printStackTrace();claims = null;}//设置需要解析的jwtreturn claims;}
}

我们可以设想下这么一个流程:
前端在请求头中设置 Authorization参数,后台再进入到controller之前,会走一个过滤器对header中的Authorization参数进行校验,也就是利用JWTUtils对token进行解析。
1.通过校验:模拟 spring Security 登录成功,把token值塞到一个变量里面。
2.未通过校验:继续走spring Security的验证流程(理论上会抛出异常)
注意以上我们分析的关键字:过滤器
因此,我们新建一个JwtAuthenticationTokenFilter 类继承OncePerRequestFilter
继承 OncePerRequestFilter 的原因:

  • 确保在一次请求中只执行一次过滤操作。OncePerRequestFilter是Spring框架提供的一个过滤器基类,它确保每个请求只通过一次,而不会重复执行过滤逻辑。
  • 当客户端发送请求时,过滤器链会按照配置的顺序对请求进行过滤。如果一个过滤器没有继承OncePerRequestFilter,它可能会在请求链中的多个位置执行,导致重复处理请求的问题。
  • 继承OncePerRequestFilter可以确保JwtAuthenticationTokenFilter在整个过滤器链中的每个请求中只执行一次,避免了多次处理同一个请求的问题。这对于执行基于JWT的身份验证和授权逻辑非常重要,因为它确保只有在一次请求中进行一次JWT的验证和解析,避免了不必要的性能开销和潜在的安全问题。

总结来说,JwtAuthenticationTokenFilter继承OncePerRequestFilter是为了保证它在过滤器链中的每个请求中只执行一次,避免了重复处理请求的问题,确保了JWT身份验证和授权逻辑的准确性和性能。

步骤二

将jwtFilter添加到Spring Security 过滤器中

JwtAuthenticationTokenFilter

/*** token过滤器 验证token有效性* 判断用户是否有效走 MyUserDetailService的 loadUserByUsername 方法** @author caojing*/
@Slf4j
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisUtils redisUtils;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException {// 从请求头中获取tokenString authToken = request.getHeader("Authorization");// 截取tokenif (authToken != null) {//验证token,获取token中的usernameClaims claims = JwtUtils.verifyJwt(authToken);if (claims == null) {throw new ServletException("token异常,请重新登录");}//从redis 获取缓存String redisKey = JwtUtils.getUserIdByJwtToken(request);UserBean userBean = redisUtils.getCacheObject(redisKey);//重新设置token的失效时间redisUtils.setCacheObject(redisKey, userBean, 30, TimeUnit.MINUTES);if (userBean != null && SecurityContextHolder.getContext().getAuthentication() == null) {//获取到值,相当于手动把session值设置到此次request中,后续就会认为已经登录,不做登录校验UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(userBean, null, userBean.getAuthorities());authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}}//继续下一个过滤器chain.doFilter(request, response);}
}

JwtAuthenticationTokenFilter添加到ScurityConfig类中

/*** Spring Security 配置类** @author caojing* @since 2023/6/14*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate MyUserDetailService userDetailService;@Autowiredprivate JwtAuthenticationTokenFilter JwtAuthenticationTokenFilter;@Overrideprotected void configure(HttpSecurity http) throws Exception {http// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterBefore(JwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//        auth.inMemoryAuthentication()
//                .passwordEncoder(new BCryptPasswordEncoder())
//                .withUser("user").password(encoder.encode("123456")).roles("USER");auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());}
}

说明:利用addFilterBefore方法,把jwt认证放到UsernamePasswordAuthenticationFilter过滤器之前。为什么要放到这里,我们下一篇文章会说。

步骤三

怎么把验证交给Spring Security

基本工作已经做完。我们还剩下一个获取token的controller。
想一想这个controller应该有什么功能?
没有使用spring Security之前,我们是不是在login获取用户输入的帐号名和密码,然后根据帐号名从数据库查询出来对应的用户信息。然后对比密码(加密后)是否正确。
使用了Spring Security之后,思考一下,哪些能用,哪些需要替换。

  1. 帐号名密码的获取肯定是要继续用的。
  2. 认证移动到了MyUserDetailService中认证,也就是使用Spring Security的 DaoAuthenticationProvider进行认证。所以原先的认证需要删除替换成DaoAuthenticationProvider认证。

上面第一个问题好解决,那么第二个问题该如何实现呢?
先说结果:

使用AuthenticationManagerauthenticate方法进行认证。

如何找到这个入口?
我们现在已知的类是DaoAuthenticationProvider,所以先从这个类开始。先看下这个类是实现AuthenticationProvider接口。先说一下这个接口的2个方法构成:

	// ~ Methods// ========================================================================================================/*** Performs authentication with the same contract as* {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}* .* * @param authentication the authentication request object.** @return a fully authenticated object including credentials. May return* <code>null</code> if the <code>AuthenticationProvider</code> is unable to support* authentication of the passed <code>Authentication</code> object. In such a case,* the next <code>AuthenticationProvider</code> that supports the presented* <code>Authentication</code> class will be tried.** @throws AuthenticationException if authentication fails.*/Authentication authenticate(Authentication authentication)throws AuthenticationException;/*** Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the* indicated <Code>Authentication</code> object.* <p>* Returning <code>true</code> does not guarantee an* <code>AuthenticationProvider</code> will be able to authenticate the presented* instance of the <code>Authentication</code> class. It simply indicates it can* support closer evaluation of it. An <code>AuthenticationProvider</code> can still* return <code>null</code> from the {@link #authenticate(Authentication)} method to* indicate another <code>AuthenticationProvider</code> should be tried.* </p>* <p>* Selection of an <code>AuthenticationProvider</code> capable of performing* authentication is conducted at runtime the <code>ProviderManager</code>.* </p>* * @param authentication** @return <code>true</code> if the implementation can more closely evaluate the* <code>Authentication</code> class presented*/boolean supports(Class<?> authentication);

这边重点注意2句话:

  1. Performs authentication with the same contract as * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
    翻译
    执行的身份认证和AuthenticationManager#authenticate这个方法具有相同的合同?
    黑人问号脸?换个说人话的:
    合同 = 契约。
    软件开发中 contract一般都翻译成契约的意思。而且契约在软件开发中特制:定义了功能、接口或方法应该具有的行为和特征的规范。当两个功能或组件具有相同的契约时,它们在执行特定操作时遵循相同的规则和约定。也就是俗称约定。
    人话:
    这个方法和AuthenticationManager#authenticate(Authentication) 具有相同的认证规则和约定
  2. Selection of an AuthenticationProvider capable of performing authentication is conducted at runtime the ProviderManager.
    翻译:
    选择一个AuthenticationProvider能够执行身份校验是在ProviderManager运行执行期间?
    人话:
    在ProviderManager运行执行期间来使用该方法判断AuthenticationProvider是否能执行身份校验

这2个方法都提到了一个类:ProviderManager。所以下一步我们看看这个类。
有点长。。。。。。。。
直接看AuthenticationManager这个接口吧:

/*** Processes an {@link Authentication} request.** @author Ben Alex*/
public interface AuthenticationManager {// ~ Methods// ========================================================================================================/*** Attempts to authenticate the passed {@link Authentication} object, returning a* fully populated <code>Authentication</code> object (including granted authorities)* if successful.* <p>* An <code>AuthenticationManager</code> must honour the following contract concerning* exceptions:* <ul>* <li>A {@link DisabledException} must be thrown if an account is disabled and the* <code>AuthenticationManager</code> can test for this state.</li>* <li>A {@link LockedException} must be thrown if an account is locked and the* <code>AuthenticationManager</code> can test for account locking.</li>* <li>A {@link BadCredentialsException} must be thrown if incorrect credentials are* presented. Whilst the above exceptions are optional, an* <code>AuthenticationManager</code> must <B>always</B> test credentials.</li>* </ul>* Exceptions should be tested for and if applicable thrown in the order expressed* above (i.e. if an account is disabled or locked, the authentication request is* immediately rejected and the credentials testing process is not performed). This* prevents credentials being tested against disabled or locked accounts.** @param authentication the authentication request object** @return a fully authenticated object including credentials** @throws AuthenticationException if authentication fails*/Authentication authenticate(Authentication authentication)throws AuthenticationException;
}

该类只有一个方法:authenticate
解释:
尝试对传递的 Authentication 对象进行身份验证,如果成功则返回一个完全填充的 Authentication 对象(包括授予的权限)。。。。。。。。。
人话:
对我们传入的Authentication对象进行身份认证,通过以后会返回Authentication 对象
简而言之。这个类AuthenticationManager 就是我们具体身份认证的入口了,但这是一个接口,具体的实现类是通过默认的ProviderManager实现。
继续看ProviderManager中的authenticate方法:

public Authentication authenticate(Authentication authentication)throws AuthenticationException {Class<? extends Authentication> toTest = authentication.getClass();AuthenticationException lastException = null;AuthenticationException parentException = null;Authentication result = null;Authentication parentResult = null;boolean debug = logger.isDebugEnabled();for (AuthenticationProvider provider : getProviders()) {if (!provider.supports(toTest)) {continue;}if (debug) {logger.debug("Authentication attempt using "+ provider.getClass().getName());}try {result = provider.authenticate(authentication);if (result != null) {copyDetails(authentication, result);break;}}catch (AccountStatusException | InternalAuthenticationServiceException e) {prepareException(e, authentication);// SEC-546: Avoid polling additional providers if auth failure is due to// invalid account statusthrow e;} catch (AuthenticationException e) {lastException = e;}}
......
}

我这里只贴出来部分代码:我们可以看到代码的主要结构是一个for循环。循环的内容是啥呢?是AuthenticationProvider的实现类。循环干什么呢?

  1. 根据AuthenticationProvider中的provider方法判断是否支持验证当前的authentication ,具体行:189行
  2. 判断具体的身份权限交给AuthenticationProviderauthenticate方法,具体行:199行

解释一下第一句话:
AuthenticationProviderauthentication 都是接口,并不是具体的实现类,所以看来比较抽象。因此,我就拿用户名密码登录方式举例。
在用户名和密码登录模式中 AuthenticationProvider的具体实现类AbstractUserDetailsAuthenticationProvider
authentication 的具体实现类是UsernamePasswordAuthenticationToken。那么验证身份流程就变成了
ProviderManager#authentication -> AbstractUserDetailsAuthenticationProvider#supports->AbstractUserDetailsAuthenticationProvider#authenticate->return UsernamePasswordAuthenticationToken
具体时序图如下所示:
获取token时序图
基于以上的流程,我们不难知道在login中需要调用authenticationManager#authenticate方法进行认证了

如何引入AuthenticationManager?

看下配置类中继承的类WebSecurityConfigurerAdapter
其中有个方法:

	/*** Override this method to expose the {@link AuthenticationManager} from* {@link #configure(AuthenticationManagerBuilder)} to be exposed as a Bean. For* example:** <pre>* &#064;Bean(name name="myAuthenticationManager")* &#064;Override* public AuthenticationManager authenticationManagerBean() throws Exception {*     return super.authenticationManagerBean();* }* </pre>** @return the {@link AuthenticationManager}* @throws Exception*/public AuthenticationManager authenticationManagerBean() throws Exception {return new AuthenticationManagerDelegator(authenticationBuilder, context);}

这很好理解吧,不需要翻译了。
Logservice 代码如下:

/*** 登录接口** @author caojing* @since 2023/6/15*/
@Slf4j
@Service
public class LoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisUtils redisUtils;public ResponseBean<String> login(String username, String password) {UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);Authentication authentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);//这边可以获取用户信息.这里getPrincipal和 JwtAuthenticationTokenFilter类中 完成token验证之后//new UsernamePasswordAuthenticationToken 塞进去的值UserBean userBean = (UserBean) authentication.getPrincipal();log.info("用户信息:{}", JSON.toJSONString(userBean));String token = JwtUtils.getJwtToken(String.valueOf(userBean.getId()), username);//每次登录都获取最新的值,redisUtils.setCacheObject(String.valueOf(userBean.getId()), userBean, 30, TimeUnit.MINUTES);return new ResponseBean<>(HttpStatus.OK.value(), "获取成功", token);}
}

SecurityConfig配置类增加

/*** Spring Security 配置类** @author caojing* @since 2023/6/14*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//......................@Override@Beanpublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}
}

启动项目

访问地址:http://127.0.0.1:8889/token
token值获取成功
测试一下token值是否有效。
先测试不带Authorization的请求:http://127.0.0.1:8889/test
在这里插入图片描述
Authorization的请求:http://127.0.0.1:8889/test
在这里插入图片描述

总结

思路:
整体思路分2个部分:

  • 登录认证获取token
    提供一个controller,将controller的地址加到spring Security 的config中不做权限控制,访问该controller,将用户名和密码的判断交给spring Security 的userDetailService处理,根据处理的返回结果决定是否生成对应的token值。

    • 如何交给Spring Security 处理认证过程:authenticationManager.authenticate()。具体是怎么找到这个入口的,详情可以看步骤三。
  • 接口认证token值

    • 加入JWT生成的工具类
    • Spring Security 提供多种认证方式,但我们需要熟悉的是UsernamePasswordAuthenticationFilter。剩下的认证方式了解即可。
    • 在了解了Spring Security的几种认证方式之后,我们需要考虑将自定义的jwtFilter加入到Srping Security的过滤器中。对应上面的步骤二。
    • 步骤二完成以后,当token值存在的时候,会把用户信息转化成UsernamePasswordAuthenticationToken,其实也不是非要这个类,任何一个实现Authentication即接口的类都可以。然后通过SecurityContextHolder.getContext().setAuthentication()方法,将用户信息设置到SecurityContextHolder
      下面是一张Spring Security的过滤器的链路图,基本上Spring Security 都是围绕着这几个过滤器进行一些功能。比如后续的异常、权限控制(选举策略)都是在过滤器中实现。具体内容咱们下个章节继续聊。
      加粗样式

习题:

  1. 为什么通过SecurityContextHolder.getContext().setAuthentication()方法就可以实现登录了。或者说SecurityContextHolder到底有什么用。
  2. Spring Security中主要分为权限和认证,认证已经讲过了,那么权限是如何控制的?(提示:也是过滤器,涉及的几个类SecurityMetadataSource GrantedAuthorityAccessDecisionManager
  3. 能否找到Spring Security中的大部分的过滤器?

下一篇主要内容是稍微介绍下Spring Security的源码,顺带解决习题中的几个问题。
上一篇文章地址:SpringBoot2.3集成Spring Security(一)


http://www.ppmy.cn/news/501868.html

相关文章

功能强大却十分小众的5款软件

有些软件虽然功能强大&#xff0c;使用便捷&#xff0c;但是却没有得到广泛的关注和推荐&#xff0c;这并不意味着它们不值得一试&#xff0c;相反&#xff0c;它们可能是你不知道的宝藏。我的任务就是要把这些隐藏的好软件分享给大家。 轻量级笔记——CintaNotes CintaNotes…

BUUCTF刷题十一道(07)

文章目录 [Zer0pts2020]Can you guess it?[CISCN2019 华北赛区 Day1 Web2]ikun[GWCTF 2019]枯燥的抽奖[WUSTCTF2020]CV Maker[NCTF2019]True XML cookbook[RCTF2015]EasySQL[CISCN2019 华北赛区 Day1 Web1]Dropbox[CISCN2019 华北赛区 Day1 Web5]CyberPunk[红明谷CTF 2021]wri…

中国一重集中采购平台的建设经历和亮点

中国一重前身为第一重型机器厂&#xff0c;是“一五”期间建设156项重点工程项目之一&#xff0c;始建于1954年&#xff0c;是中央管理的涉及国家安全和国民经济命脉的国有重要骨干企业之一&#xff0c;是国家创新型试点企业、国家高新技术企业&#xff0c;拥有国家级企业技术中…

热红外图像增强算法综述

热红外图像增强是热成像领域中的重要技术之一&#xff0c;主要用于提高热红外图像的质量和可视化效果。热红外图像由于其独特的物理特性和应用场景&#xff0c;与可见光图像相比具有不同的特征和挑战。本文将介绍热红外图像增强算法&#xff0c;并详细说明四个以上的具体算法&a…

uniapp 获取手机定位

原文地址&#xff1a;https://blog.csdn.net/qq_42778001/article/details/104922059

联想在手机市场没有找到自己的定位错失机会

联想手机在智能手机市场上错失了机会&#xff0c;因为他们没有及时跟进市场趋势&#xff0c;而停留在功能机时代。同时&#xff0c;他们的手机产品线过于杂乱&#xff0c;缺乏品牌统一的设计风格和产品定位。此外&#xff0c;联想手机的营销推广策略也相对落后&#xff0c;缺乏…

手机定位--GPS定位,基站定位,辅助定位

现在手机中的定位主要分成三类&#xff1a; GPS定位&#xff1a;GPS是全球定位系统(Global zhidaoPositioning System)的缩写形式&#xff0c;它是一种基于卫星的定位系统&#xff0c;用于获得地理位置信息以及准确的通用协调时间。 该系统由美国政府放置在轨道中的24颗卫星组…

手机位置定位

通过LAC&#xff08;Location Area Code&#xff0c;位置区域码&#xff09;和CID&#xff08;Cell Identity&#xff0c;基站编号&#xff0c;是个16位的数据&#xff08;范围是0到65535&#xff09;可以查询手机接入的基站的位置&#xff0c;从而初步确定手机用户的位置。 获…