Spring?Security+JWT簡(jiǎn)述(附源碼)
一. 什么是Spring Security
Spring Security是Spring家族的一個(gè)安全管理框架, 相比于另一個(gè)安全框架Shiro, 它具有更豐富的功能。一般中大型項(xiàng)目都是使用SpringSecurity做安全框架, 而Shiro上手比較簡(jiǎn)單
spring security 的核心功能:
- 認(rèn)證(你是誰(shuí)): 只有你的用戶名或密碼正確才能訪問(wèn)某些資源
- 授權(quán)(你能干嘛): 當(dāng)前用戶具有哪些功能, 將資源進(jìn)行劃分, 如在公司中分為普通資料和高級(jí)資料, 只有經(jīng)理用戶以上才能訪文高級(jí)資料, 其他人只能擁有訪問(wèn)普通資料的權(quán)限。
1. 登陸校驗(yàn)的流程
2. SpringSecurity基礎(chǔ)案例
首先創(chuàng)建一個(gè)Springboot的項(xiàng)目
添加依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
創(chuàng)建一個(gè)controller類
@RestController public class TestController { @GetMapping("/hello") public String hello() { return "hello"; } }
啟動(dòng)項(xiàng)目訪問(wèn)http://localhost:8080/login
, 發(fā)現(xiàn)頁(yè)面并沒(méi)有hello字符, 下圖是SpringSeurity默認(rèn)的登陸界面, 默認(rèn)用戶名為user, 密碼為啟動(dòng)項(xiàng)目時(shí)在輸出框中的內(nèi)容
在實(shí)際項(xiàng)目中, 顯然不能使用默認(rèn)的登陸界面, 所以我們需要自定義登陸認(rèn)證和授權(quán)
二. Spring Security原理流程
SpringSecurity底層實(shí)現(xiàn)是一系列過(guò)濾器鏈
默認(rèn)自動(dòng)配置的過(guò)濾器
過(guò)濾器 | 作用 |
---|---|
WebAsyncManagerIntegrationFilter | 將WebAsyncManger與SpringSecurity上下文進(jìn)行集成 |
SecurityContextPersistenceFilter | 在處理請(qǐng)求之前, 將安全信息加載到SecurityContextHolder中 |
HeaderWriterFilter | 處理頭信息假如響應(yīng)中 |
CsrfFilter | 處理CSRF攻擊 |
LogoutFilter | 處理注銷登錄 |
UsernamePasswordAuthenticationFilter | 處理表單登錄 |
DefaultLoginPageGeneratingFilter | 配置默認(rèn)登錄頁(yè)面 |
DefaultLogoutPageGeneratingFilter | 配置默認(rèn)注銷頁(yè)面 |
BasicAuthenticationFilter | 處理HttpBasic登錄 |
RequestCacheAwareFilter | 處理請(qǐng)求緩存 |
SecurityContextHolderAwareRequestFilter | 包裝原始請(qǐng)求 |
AnonymousAuthenticationFilter | 配置匿名認(rèn)證 |
SessionManagementFilter | 處理session并發(fā)問(wèn)題 |
ExceptionTranslationFilter | 處理認(rèn)證/授權(quán)中的異常 |
FilterSecurityInterceptor | 處理授權(quán)相關(guān) |
下圖是主要的過(guò)濾器
上圖只畫出了核心的過(guò)濾器
UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理登陸頁(yè)面填寫的用戶名和密碼的登陸請(qǐng)求
ExceptionTranslationFilter: 處理過(guò)濾器鏈中拋出的任何AccessDeniedException和AuthenticationException異常
FilterSecurityInterceptor: 負(fù)責(zé)權(quán)限校驗(yàn)的過(guò)濾器
1. 大致流程
(1) 下面是UsernamePasswordAuthenticationFilter
中的attemptAuthentication
方法, 該方法會(huì)將前端發(fā)送的用戶名和密碼封裝為UsernamePasswordAuthenticationToken
對(duì)象, 該對(duì)象是Authentication
對(duì)象的實(shí)現(xiàn)類
注意: attemptAuthentication
方法主要處理視圖表單認(rèn)證, 現(xiàn)今都是前后端分離項(xiàng)目導(dǎo)致不能使用該方法進(jìn)行攔截, 所以我們需要自己實(shí)現(xiàn)一個(gè)過(guò)濾器覆蓋或者在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)
, 將未認(rèn)證的Authentication
對(duì)象傳入AuthenticationManager
, 進(jìn)入authenticate
方法我們看到AuthenticationManager
是一個(gè)接口, 該接口主要做認(rèn)證管理, 它的默認(rèn)實(shí)現(xiàn)類是ProviderManager
public interface AuthenticationManager { Authentication authenticate(Authentication var1) throws AuthenticationException; }
(3) 在SpringSecurity中, 在項(xiàng)目中支持多種不同方式的認(rèn)證方式, 不同的認(rèn)證方式對(duì)應(yīng)不同的AuthenticationProvider
, 多個(gè)AuthenticationProvider
組成一個(gè)列表, 這個(gè)列表由ProviderManager
代理, 在ProviderManager
中遍歷列表中的每一個(gè)AuthenticationProvider
進(jìn)行認(rèn)證
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(); // 迭代遍歷認(rèn)證列表 Iterator var8 = this.getProviders().iterator(); while(var8.hasNext()) { // 取出當(dāng)前認(rèn)證 AuthenticationProvider provider = (AuthenticationProvider)var8.next(); // 當(dāng)前認(rèn)證是否支持當(dāng)前的用戶名和密碼信息 if (provider.supports(toTest)) { if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 開(kāi)始做認(rèn)證處理 result = provider.authenticate(authentication); if (result != null) { // 認(rèn)證成功時(shí)候返回 this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var13) { this.prepareException(var13, authentication); throw var13; } catch (AuthenticationException var14) { lastException = var14; } } } // 不支持當(dāng)前認(rèn)證并且parent支持該認(rèn)證 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
可以配置一個(gè)AuthenticationManager
作為parent, 當(dāng)ProviderManager
認(rèn)證失敗后, 可以進(jìn)入parent中再次進(jìn)行認(rèn)證, 通常由ProviderManager
來(lái)充當(dāng)parent的角色, 即ProviderManager
是ProviderManager
的parentProviderManager
可以有多個(gè), 而多個(gè)ProviderManager
共用一個(gè)parent
(4) 當(dāng)前AuthenticationProvider
支持認(rèn)證時(shí), 會(huì)進(jìn)入AuthenticationProvider
的authenticate
方法, 而AuthenticationProvider
是一個(gè)接口, 它的實(shí)現(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"); }); // 獲取當(dāng)前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 { // 調(diào)用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
方法, 但是實(shí)現(xiàn)該方法的對(duì)象是DaoAuthenticationProvider
, 該對(duì)象重寫了retrieveUser
方法, 在retrieveUser
方法中, 可以看到調(diào)用了UserDetailsService
的loadUserByUsername()
方法, 該方法用來(lái)根據(jù)用戶名查詢內(nèi)存或者其他數(shù)據(jù)源中的用戶. 默認(rèn)是基于內(nèi)存查找, 我們可以自定義為數(shù)據(jù)庫(kù)查詢. 查詢后的結(jié)果封裝成UserDetails
對(duì)象, 該對(duì)象包含用戶名、加密密碼、權(quán)限以及賬戶相關(guān)信息. 密碼的加密處理是SpringSecurity幫我們處理
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { this.prepareTimingAttackProtection(); try { // 調(diào)用該方法返回一個(gè)UserDetails 對(duì)象 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主要用于用戶登陸鑒權(quán), 在之前可能會(huì)使用session和token認(rèn)證, 下面簡(jiǎn)述三者session和JWT的區(qū)別
Session
用戶向服務(wù)器發(fā)送一個(gè)請(qǐng)求時(shí), 服務(wù)器并不知道該請(qǐng)求是誰(shuí)發(fā)的, 所以在用戶發(fā)送登錄請(qǐng)求時(shí), 服務(wù)器會(huì)將用戶提交的用戶名和密碼等信息保存在session會(huì)話中(一段內(nèi)存空間)。同時(shí)服務(wù)器保存的用戶信息會(huì)生成一個(gè)sessionid(相當(dāng)于用戶信息是一個(gè)value值, 而sessionid是value值的key)返回給客戶端, 客戶端將sessionid保存到cookie中, 等到下一次請(qǐng)求客戶端會(huì)將cookie一同請(qǐng)求給服務(wù)器做認(rèn)證
如果用戶過(guò)多, 必然會(huì)耗費(fèi)大量?jī)?nèi)存, 在cookie中存放sessionid會(huì)存在暴露用戶信息的風(fēng)險(xiǎn)
Token
token是一串隨機(jī)的字符串也叫令牌, 其原理和session類似, 當(dāng)用戶登錄時(shí), 提交的用戶名和密碼等信息請(qǐng)求給服務(wù)端, 服務(wù)端會(huì)根據(jù)用戶名或者其他信息生成一個(gè)token而不是sessionid, 這和sessionid唯一區(qū)別就是, token不再存儲(chǔ)用戶信息, 客戶端下一次請(qǐng)求會(huì)攜帶token, 此時(shí)服務(wù)器根據(jù)此次token進(jìn)行認(rèn)證。
token認(rèn)證時(shí)也會(huì)到數(shù)據(jù)庫(kù)中查詢, 會(huì)造成數(shù)據(jù)庫(kù)壓力過(guò)大。
JWT
JWT將登錄時(shí)所有信息都存在自己身上, 并且以json格式存儲(chǔ), JWT不依賴Redis或者數(shù)據(jù)庫(kù), JWT安全性不太好, 所以不能存儲(chǔ)敏感信息
2. SpringSecurity集成JWT
(1) 認(rèn)證配置
a) 配置SpringSecurity
首先配置一個(gè)SpringSecurity
的配置類, 因?yàn)槭腔贘WT進(jìn)行認(rèn)證, 所以需要在配置中禁用session機(jī)制, 并不是禁用整個(gè)系統(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機(jī)制 http.csrf().disable() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() // 指定某些接口不需要通過(guò)驗(yàn)證即可訪問(wèn)。像登陸、注冊(cè)接口肯定是不需要認(rèn)證的 .antMatchers("/sec/login").permitAll() .anyRequest().authenticated() // 自定義權(quán)限配置 .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); // 添加自定義未授權(quán)和未登陸結(jié)果返回 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) 實(shí)現(xiàn)登錄接口
先按照正常流程, 實(shí)現(xiàn)一個(gè)登錄的接口然后在業(yè)務(wù)層中實(shí)現(xiàn)
@PostMapping("/login") public Res login(@RequestBody User user, HttpServletRequest request) { return userService.login(user, request); }
在業(yè)務(wù)層中, 首先對(duì)密碼和用戶名進(jìn)行檢驗(yàn), 然后更新security登錄用戶對(duì)象, 在此之前我們先來(lái)認(rèn)識(shí)幾個(gè)在SpringSecurity
中重要的變量
Authentication
: 存儲(chǔ)了認(rèn)證信息, 代表登錄用戶SecurityContext
: 上下文對(duì)象, 用來(lái)獲取Authentication
(用戶信息)SecurityContextHolder
: 上下文管理對(duì)象, 用來(lái)在程序任何地方獲取SecurityContext
UserDetails
: 存儲(chǔ)了用戶的基本信息, 以及用戶權(quán)限、是否被禁用等
在Authentication
中的認(rèn)證信息有
Principal
: 用戶信息Credentials
: 用戶憑證, 一般是密碼Authorities
: 用戶權(quán)限
@Override public Res login(User user, HttpServletRequest request) { String username = user.getUsername(); String password = user.getPassword(); // 登陸 檢測(cè) UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(null == userDetails || !passwordEncoder.matches(password, userDetails.getPassword())) { return Res.error("用戶名或密碼不正確!"); } // 更新security登錄用戶對(duì)象 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 創(chuàng)建一個(gè)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ù)據(jù)庫(kù)或者緩存中查詢用戶提交的用戶名以及用戶的權(quán)限信息, 將這些信息保存在userDetails
中
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken
實(shí)現(xiàn)了Authentication
, 也就是說(shuō)此時(shí)將userDetails中的信息以及權(quán)限信息存放在Authentication
中
創(chuàng)建Token需要JWT的工具類, 在網(wǎng)上隨便找個(gè)都可以, 大致都一樣, 這個(gè)只需要知道就行了
c) 過(guò)濾請(qǐng)求
在原生SpringSecurity
中默認(rèn)的攔截在UsernamePasswordAuthenticationFilter
這個(gè)類中,該類主要攔截表單提交的用戶名和密碼, 顯然在前后端分離項(xiàng)目中不適用, 而且我們用到了JWT的驗(yàn)證方式, 前端每次請(qǐng)求都需要帶上token, 所以我們需要在后端對(duì)每個(gè)請(qǐng)求進(jìn)行提前過(guò)濾攔截
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 { // 請(qǐng)求頭中獲取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() 獲取上下文對(duì)象中認(rèn)證信息 if(null != username && null == SecurityContextHolder.getContext().getAuthentication()) { // 自定義數(shù)據(jù)源獲取用戶信息 UserDetails userDetails = userDetailsService.loadUserByUsername(username); // 驗(yàn)證token是否有效 驗(yàn)證token用戶名和存儲(chǔ)的用戶名是否一致以及是否在有效期內(nèi), 重新設(shè)置用戶對(duì)象 if(jwtTokenUtil.validateToken(authToken, userDetails)) { // 重新將用戶信息封裝到UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // 將信息存入上下文對(duì)象 SecurityContextHolder.getContext().setAuthentication(authenticationToken); } } } filterChain.doFilter(request,response); } }
該過(guò)濾器主要做的是:
- 提取前端發(fā)送的請(qǐng)求頭信息, 根據(jù)JWT的工具類獲取用戶名
- 如果請(qǐng)求頭具有有效的字符串(也就是擁有用戶信息)并且上下文對(duì)象存在用戶信息(數(shù)據(jù)庫(kù)或者緩存中查的用戶信息)則直接到下一個(gè)過(guò)濾器, 否則請(qǐng)求頭中有信息而當(dāng)前上下文對(duì)象沒(méi)有存儲(chǔ)用戶信息則將請(qǐng)求頭中的用戶在數(shù)據(jù)層驗(yàn)證之后重新放入上下文對(duì)象中(UsernamePasswordAuthenticationToken)。
- 如果當(dāng)前用戶沒(méi)有登錄或者沒(méi)有token信息(可能是token過(guò)期), 而當(dāng)前請(qǐng)求的地址符合權(quán)限中包含的地址(也就是數(shù)據(jù)庫(kù)中存在的), 則會(huì)進(jìn)入權(quán)限驗(yàn)證(下面會(huì)講)
當(dāng)然以上的邏輯可以自己自定義, 不管以上什么情況都會(huì)進(jìn)入權(quán)限驗(yàn)證
要讓這個(gè)過(guò)濾器加入到SpringSecurity
的過(guò)濾器鏈中, 就需要在SecurityConfig
類的configure
方法添加下面一條語(yǔ)句, addFilterBefore()
將jwtAuthencationTokenFilter()
, 放在UsernamePasswordAuthenticationFilter
之前
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
(2) 權(quán)限配置
在一個(gè)項(xiàng)目中, 不同的用戶需要具有不同的權(quán)限, 我們?cè)趺磳?duì)用戶進(jìn)行區(qū)分呢?
a) RBAC權(quán)限表
將用戶、角色和權(quán)限綁定,這樣可以知道某個(gè)用戶具有哪些角色, 而某個(gè)角色對(duì)應(yīng)有哪些權(quán)限(能干什么,不能干什么),這樣就知道哪些用戶擁有的角色和權(quán)限信息。
基于以上的想法, 我們需要三張實(shí)體表, 還需要兩張多對(duì)多的關(guān)系表, 這樣就構(gòu)成了RBAC的五張表
b) 授權(quán)流程
在SpringSecurity中授權(quán)的過(guò)濾器是FilterSecurityInterceptor
默認(rèn)的流程
- 調(diào)用
SecurityMetadataSource
獲取當(dāng)前請(qǐng)求的鑒權(quán)規(guī)則 - 接著調(diào)用
AccessDecisionManager
來(lái)校驗(yàn)當(dāng)前用戶的是否擁有當(dāng)前權(quán)限 - 如果有權(quán)限就放行, 否則拋出異常, 該異常則會(huì)被
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("尚未登錄, 請(qǐng)登錄"); } if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) { return; } } } throw new AccessDeniedException("權(quán)限不足, 請(qǐng)聯(lián)系管理員!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
FilterInvocationSecurityMetadataSource
繼承SecurityMetadataSource
在getAttributes
方法中, o參數(shù)封裝了request
的相關(guān)信息, 可以從中獲取請(qǐng)求的方法和URL等信息
然后menus得到的是當(dāng)前數(shù)據(jù)層中所有的權(quán)限路徑, 接著循環(huán)所有的路徑信息與當(dāng)前請(qǐng)求的方法和URL進(jìn)行驗(yàn)證, 如果在數(shù)據(jù)層中沒(méi)有當(dāng)前請(qǐng)求則返回null, 否則將該權(quán)限的在數(shù)據(jù)層中的信息返回
c) 自定義AccessDecisionManager
如果在SecurityMetadataSource
中有權(quán)限信息, 則會(huì)進(jìn)入該方法
@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("尚未登錄, 請(qǐng)登錄"); } if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) { return; } } } throw new AccessDeniedException("權(quán)限不足, 請(qǐng)聯(lián)系管理員!"); } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
首先來(lái)看幾個(gè)變量
- ConfigAttribute: 這個(gè)是鑒權(quán)的規(guī)則, 根據(jù)自己項(xiàng)目設(shè)定, 我們這里填入的是當(dāng)前請(qǐng)求和數(shù)據(jù)層中相匹配的權(quán)限信息id
- GrantedAuthority: 當(dāng)前認(rèn)證用戶所擁有的權(quán)限信息
在上述的decide
方法中, 主要驗(yàn)證了用戶所擁有的權(quán)限和當(dāng)前請(qǐng)求的權(quán)限信息是否一致
如果不一致, 則拋出異常, 被AccessDeniedHandler
處理
d) 自定義AccessDeniedHandler
自定義返回json格式數(shù)據(jù)
@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("權(quán)限不足, 請(qǐng)聯(lián)系管理員!"); bean.setCode(403); out.write(new ObjectMapper().writeValueAsString(bean)); out.flush(); out.close(); } }
e) 在SpringSecurity
中的配置
在configure方法中, 進(jìn)行了動(dòng)態(tài)權(quán)限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(customUrlDecisionManager); o.setSecurityMetadataSource(customFilter); return o; } })
插入: 還有一個(gè)認(rèn)證異常處理
- 用戶首次登錄且驗(yàn)證成功, 此時(shí)正常用戶權(quán)限授權(quán)
- 請(qǐng)求數(shù)據(jù)時(shí), 非首次登錄, 如果沒(méi)有攜帶token(token過(guò)期), 又或者沒(méi)有登錄訪問(wèn)內(nèi)部路徑時(shí), 說(shuō)明沒(méi)有認(rèn)證權(quán)限不能訪問(wèn), 拋出未登錄異常
- 請(qǐng)求數(shù)據(jù)時(shí), 有token信息, 而上下文對(duì)象中沒(méi)有用戶信息, 則會(huì)重新將用戶信息放入上下文對(duì)象中, 接著進(jìn)入權(quán)限驗(yàn)證, 如果用戶擁有該權(quán)限則放行, 如果沒(méi)有該權(quán)限則拋出權(quán)限不足異常
在configure中配置未登錄和未授權(quán)異常處理
http.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthoricationEntryPoint);
四. 總結(jié)
其實(shí)以上配置還有很多漏洞, 比如token的過(guò)期時(shí)間, 當(dāng)用戶上一秒還在請(qǐng)求數(shù)據(jù), 下一秒token過(guò)期, 則會(huì)造成用戶需要重新登錄, 顯然不合適
這是項(xiàng)目的地址 Github下載
到此這篇關(guān)于Spring Security+JWT簡(jiǎn)述的文章就介紹到這了,更多相關(guān)Spring Security JWT簡(jiǎn)述內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MyBatis工廠類封裝與簡(jiǎn)化實(shí)現(xiàn)
工廠類的目的是將對(duì)象的創(chuàng)建邏輯封裝在一個(gè)類中,以便客戶端代碼無(wú)需了解具體的實(shí)現(xiàn)細(xì)節(jié),本文主要介紹了MyBatis工廠類封裝與簡(jiǎn)化實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-01-01idea如何自動(dòng)生成serialVersionUID
這篇文章主要介紹了idea如何自動(dòng)生成serialVersionUID,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02SpringSecurity 手機(jī)號(hào)登錄功能實(shí)現(xiàn)
這篇文章主要介紹了SpringSecurity 手機(jī)號(hào)登錄功能實(shí)現(xiàn),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-12-12springboot攔截器Interceptor的使用,你都了解嗎
springmvc 中的攔截器可以對(duì)請(qǐng)求進(jìn)行判別,在請(qǐng)求到達(dá)控制器之前,把非法的請(qǐng)求給攔截掉下面來(lái)說(shuō)一說(shuō), 它在springboot中的使用,感興趣的朋友一起看看吧2021-07-07關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性
這篇文章主要介紹了關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性,需要的朋友可以參考下2023-07-07