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類(lèi)
@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 | 處理注銷(xiāo)登錄 |
| UsernamePasswordAuthenticationFilter | 處理表單登錄 |
| DefaultLoginPageGeneratingFilter | 配置默認(rèn)登錄頁(yè)面 |
| DefaultLogoutPageGeneratingFilter | 配置默認(rèn)注銷(xiāo)頁(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ò)濾器

上圖只畫(huà)出了核心的過(guò)濾器
UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理登陸頁(yè)面填寫(xiě)的用戶名和密碼的登陸請(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)類(lèi)
注意: 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)類(lèi)是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)類(lèi)是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ì)象重寫(xiě)了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類(lèi)似, 當(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的配置類(lèi), 因?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)在程序任何地方獲取SecurityContextUserDetails: 存儲(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的工具類(lèi), 在網(wǎng)上隨便找個(gè)都可以, 大致都一樣, 這個(gè)只需要知道就行了
c) 過(guò)濾請(qǐng)求
在原生SpringSecurity中默認(rèn)的攔截在UsernamePasswordAuthenticationFilter這個(gè)類(lèi)中,該類(lèi)主要攔截表單提交的用戶名和密碼, 顯然在前后端分離項(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的工具類(lèi)獲取用戶名
- 如果請(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類(lèi)的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)文章
Java實(shí)現(xiàn)Word/Excel/TXT轉(zhuǎn)PDF的方法
這篇文章主要介紹了Java實(shí)現(xiàn)Word/Excel/TXT轉(zhuǎn)PDF的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-01-01
JavaWeb中導(dǎo)出excel文件的簡(jiǎn)單方法
下面小編就為大家?guī)?lái)一篇JavaWeb中導(dǎo)出excel文件的簡(jiǎn)單方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-10-10
java中多個(gè)@Scheduled定時(shí)器不執(zhí)行的解決方法
在應(yīng)用開(kāi)發(fā)中經(jīng)常需要一些周期性的操作,比如每5分鐘執(zhí)行某一操作等,這篇文章主要給大家介紹了關(guān)于java中多個(gè)@Scheduled定時(shí)器不執(zhí)行的解決方法,需要的朋友可以參考下2023-04-04
Sax解析xml_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Sax解析xml,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
java中實(shí)現(xiàn)Comparable接口實(shí)現(xiàn)自定義排序的示例
下面小編就為大家?guī)?lái)一篇java中實(shí)現(xiàn)Comparable接口實(shí)現(xiàn)自定義排序的示例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-09-09
Spring中@RestControllerAdvice注解的使用詳解
這篇文章主要介紹了Spring中@RestControllerAdvice注解的使用詳解,@RestControllerAdvice是一個(gè)組合注解,由@ControllerAdvice、@ResponseBody組成,而@ControllerAdvice繼承了@Component,需要的朋友可以參考下2024-01-01
Java使用fill()數(shù)組填充的實(shí)現(xiàn)
這篇文章主要介紹了Java使用fill()數(shù)組填充的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01

