Spring Security JWT是如何实现用户认证和授权的?

2026-04-19 13:001阅读0评论SEO教程
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3188个文字,预计阅读时间需要13分钟。

Spring Security JWT是如何实现用户认证和授权的?

@TOC一. 什么是Spring SecuritySpring Security是Spring家族中的一个安全管理框架,相比另一个安全框架Shiro,它具有更丰富的功能。在大型项目中,通常使用Spring Security作为安全框架,而Shiro上手则相对简单。

@TOC

一. 什么是Spring Security

Spring Security是Spring家族的一个安全管理框架, 相比于另一个安全框架Shiro, 它具有更丰富的功能。一般中大型项目都是使用SpringSecurity做安全框架, 而Shiro上手比较简单

spring security 的核心功能:

  • 认证(你是谁): 只有你的用户名或密码正确才能访问某些资源
  • 授权(你能干嘛): 当前用户具有哪些功能, 将资源进行划分, 如在公司中分为普通资料和高级资料, 只有经理用户以上才能访文高级资料, 其他人只能拥有访问普通资料的权限。

1. 登陆校验的流程

2. SpringSecurity基础案例

首先创建一个Springboot的项目

添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

创建一个controller类

@RestController public class TestController { @GetMapping("/hello") public String hello() { return "hello"; } }

启动项目访问localhost:8080/login, 发现页面并没有hello字符, 下图是SpringSeurity默认的登陆界面, 默认用户名为user, 密码为启动项目时在输出框中的内容

在实际项目中, 显然不能使用默认的登陆界面, 所以我们需要自定义登陆认证和授权

二. Spring Security原理流程

SpringSecurity底层实现是一系列过滤器链

默认自动配置的过滤器

过滤器

作用

WebAsyncManagerIntegrationFilter

将WebAsyncManger与SpringSecurity上下文进行集成

SecurityContextPersistenceFilter

在处理请求之前, 将安全信息加载到SecurityContextHolder中

HeaderWriterFilter

处理头信息假如响应中

CsrfFilter

处理CSRF攻击

LogoutFilter

处理注销登录

UsernamePasswordAuthenticationFilter

处理表单登录

DefaultLoginPageGeneratingFilter

配置默认登录页面

DefaultLogoutPageGeneratingFilter

配置默认注销页面

BasicAuthenticationFilter

处理HttpBasic登录

RequestCacheAwareFilter

处理请求缓存

SecurityContextHolderAwareRequestFilter

包装原始请求

AnonymousAuthenticationFilter

配置匿名认证

SessionManagementFilter

处理session并发问题

ExceptionTranslationFilter

处理认证/授权中的异常

FilterSecurityInterceptor

处理授权相关

下图是主要的过滤器

上图只画出了核心的过滤器

UsernamePasswordAuthenticationFilter: 负责处理登陆页面填写的用户名和密码的登陆请求

ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException异常

Spring Security JWT是如何实现用户认证和授权的?

FilterSecurityInterceptor: 负责权限校验的过滤器

1. 大致流程

(1) 下面是UsernamePasswordAuthenticationFilter中的attemptAuthentication方法, 该方法会将前端发送的用户名和密码封装为UsernamePasswordAuthenticationToken对象, 该对象是Authentication对象的实现类

注意: attemptAuthentication方法主要处理视图表单认证, 现今都是前后端分离项目导致不能使用该方法进行拦截, 所以我们需要自己实现一个过滤器覆盖或者在UsernamePasswordAuthenticationFilter之前做用户名和密码拦截处理.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }

(2) 返回getAuthenticationManager.authenticate(authRequest), 将未认证的Authentication对象传入AuthenticationManager , 进入authenticate方法我们看到AuthenticationManager是一个接口, 该接口主要做认证管理, 它的默认实现类是ProviderManager

public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException; }

(3) 在SpringSecurity中, 在项目中支持多种不同方式的认证方式, 不同的认证方式对应不同的AuthenticationProvider, 多个AuthenticationProvider 组成一个列表, 这个列表由ProviderManager代理, 在ProviderManager中遍历列表中的每一个AuthenticationProvider进行认证

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(); // 迭代遍历认证列表 Iterator var8 = this.getProviders().iterator(); while(var8.hasNext()) { // 取出当前认证 AuthenticationProvider provider = (AuthenticationProvider)var8.next(); // 当前认证是否支持当前的用户名和密码信息 if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 开始做认证处理 result = provider.authenticate(authentication); if (result != null) { // 认证成功时候返回 this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } } // 不支持当前认证并且parent支持该认证 if (result == null && this.parent != null) { try { result = parentResult = this.parent.authenticate(authentication); } catch (ProviderNotFoundException var11) { } catch (AuthenticationException var12) { parentException = var12; lastException = var12; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } }

拓展:ProviderManager可以配置一个AuthenticationManager作为parent, 当ProviderManager认证失败后, 可以进入parent中再次进行认证, 通常由ProviderManager来充当parent的角色, 即ProviderManagerProviderManager的parentProviderManager可以有多个, 而多个ProviderManager共用一个parent

(4) 当前AuthenticationProvider支持认证时, 会进入AuthenticationProviderauthenticate方法, 而AuthenticationProvider是一个接口, 它的实现类是AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); // 获取当前authentication的信息 String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 在缓存中查看username UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // 调用retrieveUser方法 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("User '" + username + "' not found"); if (this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); // 密码的加密处理 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); }

(5) retrieveUserAbstractUserDetailsAuthenticationProvider中有retrieveUser方法, 但是实现该方法的对象是DaoAuthenticationProvider, 该对象重写了retrieveUser方法, 在retrieveUser方法中, 可以看到调用了UserDetailsServiceloadUserByUsername()方法, 该方法用来根据用户名查询内存或者其他数据源中的用户. 默认是基于内存查找, 我们可以自定义为数据库查询. 查询后的结果封装成UserDetails 对象, 该对象包含用户名、加密密码、权限以及账户相关信息. 密码的加密处理是SpringSecurity帮我们处理

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { // 调用该方法返回一个UserDetails 对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } }

三. JWT

1. 什么是JWT?

JWT主要用于用户登陆鉴权, 在之前可能会使用session和token认证, 下面简述三者session和JWT的区别

Session

用户向服务器发送一个请求时, 服务器并不知道该请求是谁发的, 所以在用户发送登录请求时, 服务器会将用户提交的用户名和密码等信息保存在session会话中(一段内存空间)。同时服务器保存的用户信息会生成一个sessionid(相当于用户信息是一个value值, 而sessionid是value值的key)返回给客户端, 客户端将sessionid保存到cookie中, 等到下一次请求客户端会将cookie一同请求给服务器做认证

如果用户过多, 必然会耗费大量内存, 在cookie中存放sessionid会存在暴露用户信息的风险

Token

token是一串随机的字符串也叫令牌, 其原理和session类似, 当用户登录时, 提交的用户名和密码等信息请求给服务端, 服务端会根据用户名或者其他信息生成一个token而不是sessionid, 这和sessionid唯一区别就是, token不再存储用户信息, 客户端下一次请求会携带token, 此时服务器根据此次token进行认证。

token认证时也会到数据库中查询, 会造成数据库压力过大。

JWT

JWT将登录时所有信息都存在自己身上, 并且以json格式存储, JWT不依赖Redis或者数据库, JWT安全性不太好, 所以不能存储敏感信息

2. SpringSecurity集成JWT

(1) 认证配置

a) 配置SpringSecurity

首先配置一个SpringSecurity的配置类, 因为是基于JWT进行认证, 所以需要在配置中禁用session机制, 并不是禁用整个系统的session功能

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserServiceImpl userDetailsService; @Autowired private LoginFilter loginFilter; @Autowired private AuthFilter authFilter; @Override protected void configure(HttpSecurity www.558idc.com/helan.html 复制请保留原URL】

本文共计3188个文字,预计阅读时间需要13分钟。

Spring Security JWT是如何实现用户认证和授权的?

@TOC一. 什么是Spring SecuritySpring Security是Spring家族中的一个安全管理框架,相比另一个安全框架Shiro,它具有更丰富的功能。在大型项目中,通常使用Spring Security作为安全框架,而Shiro上手则相对简单。

@TOC

一. 什么是Spring Security

Spring Security是Spring家族的一个安全管理框架, 相比于另一个安全框架Shiro, 它具有更丰富的功能。一般中大型项目都是使用SpringSecurity做安全框架, 而Shiro上手比较简单

spring security 的核心功能:

  • 认证(你是谁): 只有你的用户名或密码正确才能访问某些资源
  • 授权(你能干嘛): 当前用户具有哪些功能, 将资源进行划分, 如在公司中分为普通资料和高级资料, 只有经理用户以上才能访文高级资料, 其他人只能拥有访问普通资料的权限。

1. 登陆校验的流程

2. SpringSecurity基础案例

首先创建一个Springboot的项目

添加依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>

创建一个controller类

@RestController public class TestController { @GetMapping("/hello") public String hello() { return "hello"; } }

启动项目访问localhost:8080/login, 发现页面并没有hello字符, 下图是SpringSeurity默认的登陆界面, 默认用户名为user, 密码为启动项目时在输出框中的内容

在实际项目中, 显然不能使用默认的登陆界面, 所以我们需要自定义登陆认证和授权

二. Spring Security原理流程

SpringSecurity底层实现是一系列过滤器链

默认自动配置的过滤器

过滤器

作用

WebAsyncManagerIntegrationFilter

将WebAsyncManger与SpringSecurity上下文进行集成

SecurityContextPersistenceFilter

在处理请求之前, 将安全信息加载到SecurityContextHolder中

HeaderWriterFilter

处理头信息假如响应中

CsrfFilter

处理CSRF攻击

LogoutFilter

处理注销登录

UsernamePasswordAuthenticationFilter

处理表单登录

DefaultLoginPageGeneratingFilter

配置默认登录页面

DefaultLogoutPageGeneratingFilter

配置默认注销页面

BasicAuthenticationFilter

处理HttpBasic登录

RequestCacheAwareFilter

处理请求缓存

SecurityContextHolderAwareRequestFilter

包装原始请求

AnonymousAuthenticationFilter

配置匿名认证

SessionManagementFilter

处理session并发问题

ExceptionTranslationFilter

处理认证/授权中的异常

FilterSecurityInterceptor

处理授权相关

下图是主要的过滤器

上图只画出了核心的过滤器

UsernamePasswordAuthenticationFilter: 负责处理登陆页面填写的用户名和密码的登陆请求

ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException异常

Spring Security JWT是如何实现用户认证和授权的?

FilterSecurityInterceptor: 负责权限校验的过滤器

1. 大致流程

(1) 下面是UsernamePasswordAuthenticationFilter中的attemptAuthentication方法, 该方法会将前端发送的用户名和密码封装为UsernamePasswordAuthenticationToken对象, 该对象是Authentication对象的实现类

注意: attemptAuthentication方法主要处理视图表单认证, 现今都是前后端分离项目导致不能使用该方法进行拦截, 所以我们需要自己实现一个过滤器覆盖或者在UsernamePasswordAuthenticationFilter之前做用户名和密码拦截处理.

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }

(2) 返回getAuthenticationManager.authenticate(authRequest), 将未认证的Authentication对象传入AuthenticationManager , 进入authenticate方法我们看到AuthenticationManager是一个接口, 该接口主要做认证管理, 它的默认实现类是ProviderManager

public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException; }

(3) 在SpringSecurity中, 在项目中支持多种不同方式的认证方式, 不同的认证方式对应不同的AuthenticationProvider, 多个AuthenticationProvider 组成一个列表, 这个列表由ProviderManager代理, 在ProviderManager中遍历列表中的每一个AuthenticationProvider进行认证

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(); // 迭代遍历认证列表 Iterator var8 = this.getProviders().iterator(); while(var8.hasNext()) { // 取出当前认证 AuthenticationProvider provider = (AuthenticationProvider)var8.next(); // 当前认证是否支持当前的用户名和密码信息 if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 开始做认证处理 result = provider.authenticate(authentication); if (result != null) { // 认证成功时候返回 this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } } // 不支持当前认证并且parent支持该认证 if (result == null && this.parent != null) { try { result = parentResult = this.parent.authenticate(authentication); } catch (ProviderNotFoundException var11) { } catch (AuthenticationException var12) { parentException = var12; lastException = var12; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } else { if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } }

拓展:ProviderManager可以配置一个AuthenticationManager作为parent, 当ProviderManager认证失败后, 可以进入parent中再次进行认证, 通常由ProviderManager来充当parent的角色, 即ProviderManagerProviderManager的parentProviderManager可以有多个, 而多个ProviderManager共用一个parent

(4) 当前AuthenticationProvider支持认证时, 会进入AuthenticationProviderauthenticate方法, 而AuthenticationProvider是一个接口, 它的实现类是AbstractUserDetailsAuthenticationProvider

public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> { return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"); }); // 获取当前authentication的信息 String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 在缓存中查看username UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // 调用retrieveUser方法 user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); } catch (UsernameNotFoundException var6) { this.logger.debug("User '" + username + "' not found"); if (this.hideUserNotFoundExceptions) { throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } throw var6; } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } catch (AuthenticationException var7) { if (!cacheWasUsed) { throw var7; } cacheWasUsed = false; user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication); this.preAuthenticationChecks.check(user); // 密码的加密处理 this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return this.createSuccessAuthentication(principalToReturn, authentication, user); }

(5) retrieveUserAbstractUserDetailsAuthenticationProvider中有retrieveUser方法, 但是实现该方法的对象是DaoAuthenticationProvider, 该对象重写了retrieveUser方法, 在retrieveUser方法中, 可以看到调用了UserDetailsServiceloadUserByUsername()方法, 该方法用来根据用户名查询内存或者其他数据源中的用户. 默认是基于内存查找, 我们可以自定义为数据库查询. 查询后的结果封装成UserDetails 对象, 该对象包含用户名、加密密码、权限以及账户相关信息. 密码的加密处理是SpringSecurity帮我们处理

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { // 调用该方法返回一个UserDetails 对象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation"); } else { return loadedUser; } } catch (UsernameNotFoundException var4) { this.mitigateAgainstTimingAttack(authentication); throw var4; } catch (InternalAuthenticationServiceException var5) { throw var5; } catch (Exception var6) { throw new InternalAuthenticationServiceException(var6.getMessage(), var6); } }

三. JWT

1. 什么是JWT?

JWT主要用于用户登陆鉴权, 在之前可能会使用session和token认证, 下面简述三者session和JWT的区别

Session

用户向服务器发送一个请求时, 服务器并不知道该请求是谁发的, 所以在用户发送登录请求时, 服务器会将用户提交的用户名和密码等信息保存在session会话中(一段内存空间)。同时服务器保存的用户信息会生成一个sessionid(相当于用户信息是一个value值, 而sessionid是value值的key)返回给客户端, 客户端将sessionid保存到cookie中, 等到下一次请求客户端会将cookie一同请求给服务器做认证

如果用户过多, 必然会耗费大量内存, 在cookie中存放sessionid会存在暴露用户信息的风险

Token

token是一串随机的字符串也叫令牌, 其原理和session类似, 当用户登录时, 提交的用户名和密码等信息请求给服务端, 服务端会根据用户名或者其他信息生成一个token而不是sessionid, 这和sessionid唯一区别就是, token不再存储用户信息, 客户端下一次请求会携带token, 此时服务器根据此次token进行认证。

token认证时也会到数据库中查询, 会造成数据库压力过大。

JWT

JWT将登录时所有信息都存在自己身上, 并且以json格式存储, JWT不依赖Redis或者数据库, JWT安全性不太好, 所以不能存储敏感信息

2. SpringSecurity集成JWT

(1) 认证配置

a) 配置SpringSecurity

首先配置一个SpringSecurity的配置类, 因为是基于JWT进行认证, 所以需要在配置中禁用session机制, 并不是禁用整个系统的session功能

@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserServiceImpl userDetailsService; @Autowired private LoginFilter loginFilter; @Autowired private AuthFilter authFilter; @Override protected void configure(HttpSecurity www.558idc.com/helan.html 复制请保留原URL】