Spring?Security+JWT簡述(附源碼)
一. 什么是Spring Security
Spring Security是Spring家族的一個安全管理框架, 相比于另一個安全框架Shiro, 它具有更豐富的功能。一般中大型項目都是使用SpringSecurity做安全框架, 而Shiro上手比較簡單
spring security 的核心功能:
- 認證(你是誰): 只有你的用戶名或密碼正確才能訪問某些資源
- 授權(你能干嘛): 當前用戶具有哪些功能, 將資源進行劃分, 如在公司中分為普通資料和高級資料, 只有經理用戶以上才能訪文高級資料, 其他人只能擁有訪問普通資料的權限。
1. 登陸校驗的流程
2. SpringSecurity基礎案例
首先創(chuàng)建一個Springboot的項目
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
創(chuàng)建一個controller類
@RestController public class TestController { @GetMapping("/hello") public String hello() { return "hello"; } }
啟動項目訪問http://localhost:8080/login
, 發(fā)現(xiàn)頁面并沒有hello字符, 下圖是SpringSeurity默認的登陸界面, 默認用戶名為user, 密碼為啟動項目時在輸出框中的內容
在實際項目中, 顯然不能使用默認的登陸界面, 所以我們需要自定義登陸認證和授權
二. Spring Security原理流程
SpringSecurity底層實現(xiàn)是一系列過濾器鏈
默認自動配置的過濾器
過濾器 | 作用 |
---|---|
WebAsyncManagerIntegrationFilter | 將WebAsyncManger與SpringSecurity上下文進行集成 |
SecurityContextPersistenceFilter | 在處理請求之前, 將安全信息加載到SecurityContextHolder中 |
HeaderWriterFilter | 處理頭信息假如響應中 |
CsrfFilter | 處理CSRF攻擊 |
LogoutFilter | 處理注銷登錄 |
UsernamePasswordAuthenticationFilter | 處理表單登錄 |
DefaultLoginPageGeneratingFilter | 配置默認登錄頁面 |
DefaultLogoutPageGeneratingFilter | 配置默認注銷頁面 |
BasicAuthenticationFilter | 處理HttpBasic登錄 |
RequestCacheAwareFilter | 處理請求緩存 |
SecurityContextHolderAwareRequestFilter | 包裝原始請求 |
AnonymousAuthenticationFilter | 配置匿名認證 |
SessionManagementFilter | 處理session并發(fā)問題 |
ExceptionTranslationFilter | 處理認證/授權中的異常 |
FilterSecurityInterceptor | 處理授權相關 |
下圖是主要的過濾器
上圖只畫出了核心的過濾器
UsernamePasswordAuthenticationFilter: 負責處理登陸頁面填寫的用戶名和密碼的登陸請求
ExceptionTranslationFilter: 處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException異常
FilterSecurityInterceptor: 負責權限校驗的過濾器
1. 大致流程
(1) 下面是UsernamePasswordAuthenticationFilter
中的attemptAuthentication
方法, 該方法會將前端發(fā)送的用戶名和密碼封裝為UsernamePasswordAuthenticationToken
對象, 該對象是Authentication
對象的實現(xiàn)類
注意: attemptAuthentication
方法主要處理視圖表單認證, 現(xiàn)今都是前后端分離項目導致不能使用該方法進行攔截, 所以我們需要自己實現(xiàn)一個過濾器覆蓋或者在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
是一個接口, 該接口主要做認證管理, 它的默認實現(xiàn)類是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的角色, 即ProviderManager
是ProviderManager
的parentProviderManager
可以有多個, 而多個ProviderManager
共用一個parent
(4) 當前AuthenticationProvider
支持認證時, 會進入AuthenticationProvider
的authenticate
方法, 而AuthenticationProvider
是一個接口, 它的實現(xiàn)類是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) retrieveUser
在AbstractUserDetailsAuthenticationProvider
中有retrieveUser
方法, 但是實現(xiàn)該方法的對象是DaoAuthenticationProvider
, 該對象重寫了retrieveUser
方法, 在retrieveUser
方法中, 可以看到調用了UserDetailsService
的loadUserByUsername()
方法, 該方法用來根據用戶名查詢內存或者其他數(shù)據源中的用戶. 默認是基于內存查找, 我們可以自定義為數(shù)據庫查詢. 查詢后的結果封裝成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的區(qū)別
Session
用戶向服務器發(fā)送一個請求時, 服務器并不知道該請求是誰發(fā)的, 所以在用戶發(fā)送登錄請求時, 服務器會將用戶提交的用戶名和密碼等信息保存在session會話中(一段內存空間)。同時服務器保存的用戶信息會生成一個sessionid(相當于用戶信息是一個value值, 而sessionid是value值的key)返回給客戶端, 客戶端將sessionid保存到cookie中, 等到下一次請求客戶端會將cookie一同請求給服務器做認證
如果用戶過多, 必然會耗費大量內存, 在cookie中存放sessionid會存在暴露用戶信息的風險
Token
token是一串隨機的字符串也叫令牌, 其原理和session類似, 當用戶登錄時, 提交的用戶名和密碼等信息請求給服務端, 服務端會根據用戶名或者其他信息生成一個token而不是sessionid, 這和sessionid唯一區(qū)別就是, token不再存儲用戶信息, 客戶端下一次請求會攜帶token, 此時服務器根據此次token進行認證。
token認證時也會到數(shù)據庫中查詢, 會造成數(shù)據庫壓力過大。
JWT
JWT將登錄時所有信息都存在自己身上, 并且以json格式存儲, JWT不依賴Redis或者數(shù)據庫, JWT安全性不太好, 所以不能存儲敏感信息
2. SpringSecurity集成JWT
(1) 認證配置
a) 配置SpringSecurity
首先配置一個SpringSecurity
的配置類, 因為是基于JWT進行認證, 所以需要在配置中禁用session機制, 并不是禁用整個系統(tǒng)的session功能
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserServiceImpl userDetailsService; @Autowired private LoginFilter loginFilter; @Autowired private AuthFilter authFilter; @Override protected void configure(HttpSecurity http) throws Exception { // 禁用session機制 http.csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() // 指定某些接口不需要通過驗證即可訪問。像登陸、注冊接口肯定是不需要認證的 .antMatchers("/sec/login").permitAll() .anyRequest().authenticated() // 自定義權限配置 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(customUrlDecisionManager); o.setSecurityMetadataSource(customFilter); return o; } }) .and() // 禁用緩存 .headers() .cacheControl(); http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class); // 添加自定義未授權和未登陸結果返回 http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthoricationEntryPoint); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 指定UserDetailService和加密器 auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
b) 實現(xiàn)登錄接口
先按照正常流程, 實現(xiàn)一個登錄的接口然后在業(yè)務層中實現(xiàn)
@PostMapping("/login") public Res login(@RequestBody User user, HttpServletRequest request) { return userService.login(user, request); }
在業(yè)務層中, 首先對密碼和用戶名進行檢驗, 然后更新security登錄用戶對象, 在此之前我們先來認識幾個在SpringSecurity
中重要的變量
Authentication
: 存儲了認證信息, 代表登錄用戶SecurityContext
: 上下文對象, 用來獲取Authentication
(用戶信息)SecurityContextHolder
: 上下文管理對象, 用來在程序任何地方獲取SecurityContext
UserDetails
: 存儲了用戶的基本信息, 以及用戶權限、是否被禁用等
在Authentication
中的認證信息有
Principal
: 用戶信息Credentials
: 用戶憑證, 一般是密碼Authorities
: 用戶權限
@Override public Res login(User user, HttpServletRequest request) { String username = user.getUsername(); String password = user.getPassword(); // 登陸 檢測 UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) { return Res.error("用戶名或密碼不正確!"); } // 更新security登錄用戶對象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 創(chuàng)建一個token String token = jwtTokenUtil.generateToken(userDetails); Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); tokenMap.put("tokenHead", tokenHead); return Res.success("登陸成功", tokenMap); }
下面這行代碼主要是在數(shù)據庫或者緩存中查詢用戶提交的用戶名以及用戶的權限信息, 將這些信息保存在userDetails
中
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken
實現(xiàn)了Authentication
, 也就是說此時將userDetails中的信息以及權限信息存放在Authentication
中
創(chuàng)建Token需要JWT的工具類, 在網上隨便找個都可以, 大致都一樣, 這個只需要知道就行了
c) 過濾請求
在原生SpringSecurity
中默認的攔截在UsernamePasswordAuthenticationFilter
這個類中,該類主要攔截表單提交的用戶名和密碼, 顯然在前后端分離項目中不適用, 而且我們用到了JWT的驗證方式, 前端每次請求都需要帶上token, 所以我們需要在后端對每個請求進行提前過濾攔截
public class JwtAuthencationTokenFilter extends OncePerRequestFilter { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 請求頭中獲取token信息 String authheader = request.getHeader(tokenHeader); // 存在token if(null != authheader && authheader.startsWith(tokenHead)) { // 去除字段名稱, 獲取真正token String authToken = authheader.substring(tokenHead.length()); // 利用token獲取用戶名 String username = jwtTokenUtil.getUserNameFromToken(authToken); // token存在用戶但未登陸 // SecurityContextHolder.getContext().getAuthentication() 獲取上下文對象中認證信息 if(null != username && null == SecurityContextHolder.getContext().getAuthentication()) { // 自定義數(shù)據源獲取用戶信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 驗證token是否有效 驗證token用戶名和存儲的用戶名是否一致以及是否在有效期內, 重新設置用戶對象 if(jwtTokenUtil.validateToken(authToken, userDetails)) { // 重新將用戶信息封裝到UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 將信息存入上下文對象 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request,response); } }
該過濾器主要做的是:
- 提取前端發(fā)送的請求頭信息, 根據JWT的工具類獲取用戶名
- 如果請求頭具有有效的字符串(也就是擁有用戶信息)并且上下文對象存在用戶信息(數(shù)據庫或者緩存中查的用戶信息)則直接到下一個過濾器, 否則請求頭中有信息而當前上下文對象沒有存儲用戶信息則將請求頭中的用戶在數(shù)據層驗證之后重新放入上下文對象中(UsernamePasswordAuthenticationToken)。
- 如果當前用戶沒有登錄或者沒有token信息(可能是token過期), 而當前請求的地址符合權限中包含的地址(也就是數(shù)據庫中存在的), 則會進入權限驗證(下面會講)
當然以上的邏輯可以自己自定義, 不管以上什么情況都會進入權限驗證
要讓這個過濾器加入到SpringSecurity
的過濾器鏈中, 就需要在SecurityConfig
類的configure
方法添加下面一條語句, addFilterBefore()
將jwtAuthencationTokenFilter()
, 放在UsernamePasswordAuthenticationFilter
之前
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
(2) 權限配置
在一個項目中, 不同的用戶需要具有不同的權限, 我們怎么對用戶進行區(qū)分呢?
a) RBAC權限表
將用戶、角色和權限綁定,這樣可以知道某個用戶具有哪些角色, 而某個角色對應有哪些權限(能干什么,不能干什么),這樣就知道哪些用戶擁有的角色和權限信息。
基于以上的想法, 我們需要三張實體表, 還需要兩張多對多的關系表, 這樣就構成了RBAC的五張表
b) 授權流程
在SpringSecurity中授權的過濾器是FilterSecurityInterceptor
默認的流程
- 調用
SecurityMetadataSource
獲取當前請求的鑒權規(guī)則 - 接著調用
AccessDecisionManager
來校驗當前用戶的是否擁有當前權限 - 如果有權限就放行, 否則拋出異常, 該異常則會被
AccessDeniedHandler
處理
c) 自定義SecurityMetadataSource
@Component public class CustomUrlDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { if(Collections.isEmpty(collection)) { return; } for (ConfigAttribute configAttribute : collection) { for (GrantedAuthority authority : authentication.getAuthorities()) { if("ROLE_ANONYMOUS".equals(authority.getAuthority())) { throw new AccessDeniedException("尚未登錄, 請登錄"); } if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) { return; } } } throw new AccessDeniedException("權限不足, 請聯(lián)系管理員!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
FilterInvocationSecurityMetadataSource
繼承SecurityMetadataSource
在getAttributes
方法中, o參數(shù)封裝了request
的相關信息, 可以從中獲取請求的方法和URL等信息
然后menus得到的是當前數(shù)據層中所有的權限路徑, 接著循環(huán)所有的路徑信息與當前請求的方法和URL進行驗證, 如果在數(shù)據層中沒有當前請求則返回null, 否則將該權限的在數(shù)據層中的信息返回
c) 自定義AccessDecisionManager
如果在SecurityMetadataSource
中有權限信息, 則會進入該方法
@Component public class CustomUrlDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { if(Collections.isEmpty(collection)) { return; } for (ConfigAttribute configAttribute : collection) { for (GrantedAuthority authority : authentication.getAuthorities()) { if("ROLE_ANONYMOUS".equals(authority.getAuthority())) { throw new AccessDeniedException("尚未登錄, 請登錄"); } if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) { return; } } } throw new AccessDeniedException("權限不足, 請聯(lián)系管理員!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
首先來看幾個變量
- ConfigAttribute: 這個是鑒權的規(guī)則, 根據自己項目設定, 我們這里填入的是當前請求和數(shù)據層中相匹配的權限信息id
- GrantedAuthority: 當前認證用戶所擁有的權限信息
在上述的decide
方法中, 主要驗證了用戶所擁有的權限和當前請求的權限信息是否一致
如果不一致, 則拋出異常, 被AccessDeniedHandler
處理
d) 自定義AccessDeniedHandler
自定義返回json格式數(shù)據
@Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); PrintWriter out = response.getWriter(); Res bean = Res.error("權限不足, 請聯(lián)系管理員!"); bean.setCode(403); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
e) 在SpringSecurity
中的配置
在configure方法中, 進行了動態(tài)權限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(customUrlDecisionManager); o.setSecurityMetadataSource(customFilter); return o; } })
插入: 還有一個認證異常處理
- 用戶首次登錄且驗證成功, 此時正常用戶權限授權
- 請求數(shù)據時, 非首次登錄, 如果沒有攜帶token(token過期), 又或者沒有登錄訪問內部路徑時, 說明沒有認證權限不能訪問, 拋出未登錄異常
- 請求數(shù)據時, 有token信息, 而上下文對象中沒有用戶信息, 則會重新將用戶信息放入上下文對象中, 接著進入權限驗證, 如果用戶擁有該權限則放行, 如果沒有該權限則拋出權限不足異常
在configure中配置未登錄和未授權異常處理
http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthoricationEntryPoint);
四. 總結
其實以上配置還有很多漏洞, 比如token的過期時間, 當用戶上一秒還在請求數(shù)據, 下一秒token過期, 則會造成用戶需要重新登錄, 顯然不合適
這是項目的地址 Github下載
到此這篇關于Spring Security+JWT簡述的文章就介紹到這了,更多相關Spring Security JWT簡述內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringSecurity 手機號登錄功能實現(xiàn)
這篇文章主要介紹了SpringSecurity 手機號登錄功能實現(xiàn),本文通過實例代碼給大家介紹的非常詳細,感興趣的朋友一起看看吧2023-12-12springboot攔截器Interceptor的使用,你都了解嗎
springmvc 中的攔截器可以對請求進行判別,在請求到達控制器之前,把非法的請求給攔截掉下面來說一說, 它在springboot中的使用,感興趣的朋友一起看看吧2021-07-07關于Spring中的@Configuration中的proxyBeanMethods屬性
這篇文章主要介紹了關于Spring中的@Configuration中的proxyBeanMethods屬性,需要的朋友可以參考下2023-07-07