Spring?Security+JWT簡述(附源碼)
一. 什么是Spring Security
Spring Security是Spring家族的一個安全管理框架, 相比于另一個安全框架Shiro, 它具有更豐富的功能。一般中大型項目都是使用SpringSecurity做安全框架, 而Shiro上手比較簡單
spring security 的核心功能:
- 認(rèn)證(你是誰): 只有你的用戶名或密碼正確才能訪問某些資源
- 授權(quán)(你能干嘛): 當(dāng)前用戶具有哪些功能, 將資源進(jìn)行劃分, 如在公司中分為普通資料和高級資料, 只有經(jīng)理用戶以上才能訪文高級資料, 其他人只能擁有訪問普通資料的權(quán)限。
1. 登陸校驗的流程

2. SpringSecurity基礎(chǔ)案例
首先創(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默認(rèn)的登陸界面, 默認(rèn)用戶名為user, 密碼為啟動項目時在輸出框中的內(nèi)容


在實際項目中, 顯然不能使用默認(rèn)的登陸界面, 所以我們需要自定義登陸認(rèn)證和授權(quán)
二. Spring Security原理流程
SpringSecurity底層實現(xiàn)是一系列過濾器鏈
默認(rèn)自動配置的過濾器

| 過濾器 | 作用 |
|---|---|
| WebAsyncManagerIntegrationFilter | 將WebAsyncManger與SpringSecurity上下文進(jìn)行集成 |
| SecurityContextPersistenceFilter | 在處理請求之前, 將安全信息加載到SecurityContextHolder中 |
| HeaderWriterFilter | 處理頭信息假如響應(yīng)中 |
| CsrfFilter | 處理CSRF攻擊 |
| LogoutFilter | 處理注銷登錄 |
| UsernamePasswordAuthenticationFilter | 處理表單登錄 |
| DefaultLoginPageGeneratingFilter | 配置默認(rèn)登錄頁面 |
| DefaultLogoutPageGeneratingFilter | 配置默認(rèn)注銷頁面 |
| BasicAuthenticationFilter | 處理HttpBasic登錄 |
| RequestCacheAwareFilter | 處理請求緩存 |
| SecurityContextHolderAwareRequestFilter | 包裝原始請求 |
| AnonymousAuthenticationFilter | 配置匿名認(rèn)證 |
| SessionManagementFilter | 處理session并發(fā)問題 |
| ExceptionTranslationFilter | 處理認(rèn)證/授權(quán)中的異常 |
| FilterSecurityInterceptor | 處理授權(quán)相關(guān) |
下圖是主要的過濾器

上圖只畫出了核心的過濾器
UsernamePasswordAuthenticationFilter: 負(fù)責(zé)處理登陸頁面填寫的用戶名和密碼的登陸請求
ExceptionTranslationFilter: 處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException異常
FilterSecurityInterceptor: 負(fù)責(zé)權(quán)限校驗的過濾器
1. 大致流程

(1) 下面是UsernamePasswordAuthenticationFilter中的attemptAuthentication方法, 該方法會將前端發(fā)送的用戶名和密碼封裝為UsernamePasswordAuthenticationToken對象, 該對象是Authentication對象的實現(xiàn)類
注意: attemptAuthentication方法主要處理視圖表單認(rèn)證, 現(xiàn)今都是前后端分離項目導(dǎo)致不能使用該方法進(jì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), 將未認(rèn)證的Authentication對象傳入AuthenticationManager , 進(jìn)入authenticate方法我們看到AuthenticationManager是一個接口, 該接口主要做認(rèn)證管理, 它的默認(rèn)實現(xiàn)類是ProviderManager
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
(3) 在SpringSecurity中, 在項目中支持多種不同方式的認(rèn)證方式, 不同的認(rèn)證方式對應(yīng)不同的AuthenticationProvider, 多個AuthenticationProvider 組成一個列表, 這個列表由ProviderManager代理, 在ProviderManager中遍歷列表中的每一個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 {
// 開始做認(rèn)證處理
result = provider.authenticate(authentication);
if (result != null) {
// 認(rèn)證成功時候返回
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可以配置一個AuthenticationManager作為parent, 當(dāng)ProviderManager認(rèn)證失敗后, 可以進(jìn)入parent中再次進(jìn)行認(rèn)證, 通常由ProviderManager來充當(dāng)parent的角色, 即ProviderManager是ProviderManager的parentProviderManager可以有多個, 而多個ProviderManager共用一個parent

(4) 當(dāng)前AuthenticationProvider支持認(rèn)證時, 會進(jìn)入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");
});
// 獲取當(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方法, 但是實現(xiàn)該方法的對象是DaoAuthenticationProvider, 該對象重寫了retrieveUser方法, 在retrieveUser方法中, 可以看到調(diào)用了UserDetailsService的loadUserByUsername()方法, 該方法用來根據(jù)用戶名查詢內(nèi)存或者其他數(shù)據(jù)源中的用戶. 默認(rèn)是基于內(nèi)存查找, 我們可以自定義為數(shù)據(jù)庫查詢. 查詢后的結(jié)果封裝成UserDetails 對象, 該對象包含用戶名、加密密碼、權(quán)限以及賬戶相關(guān)信息. 密碼的加密處理是SpringSecurity幫我們處理
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
// 調(diào)用該方法返回一個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主要用于用戶登陸鑒權(quán), 在之前可能會使用session和token認(rèn)證, 下面簡述三者session和JWT的區(qū)別
Session
用戶向服務(wù)器發(fā)送一個請求時, 服務(wù)器并不知道該請求是誰發(fā)的, 所以在用戶發(fā)送登錄請求時, 服務(wù)器會將用戶提交的用戶名和密碼等信息保存在session會話中(一段內(nèi)存空間)。同時服務(wù)器保存的用戶信息會生成一個sessionid(相當(dāng)于用戶信息是一個value值, 而sessionid是value值的key)返回給客戶端, 客戶端將sessionid保存到cookie中, 等到下一次請求客戶端會將cookie一同請求給服務(wù)器做認(rèn)證
如果用戶過多, 必然會耗費(fèi)大量內(nèi)存, 在cookie中存放sessionid會存在暴露用戶信息的風(fēng)險
Token
token是一串隨機(jī)的字符串也叫令牌, 其原理和session類似, 當(dāng)用戶登錄時, 提交的用戶名和密碼等信息請求給服務(wù)端, 服務(wù)端會根據(jù)用戶名或者其他信息生成一個token而不是sessionid, 這和sessionid唯一區(qū)別就是, token不再存儲用戶信息, 客戶端下一次請求會攜帶token, 此時服務(wù)器根據(jù)此次token進(jìn)行認(rèn)證。
token認(rèn)證時也會到數(shù)據(jù)庫中查詢, 會造成數(shù)據(jù)庫壓力過大。
JWT
JWT將登錄時所有信息都存在自己身上, 并且以json格式存儲, JWT不依賴Redis或者數(shù)據(jù)庫, JWT安全性不太好, 所以不能存儲敏感信息
2. SpringSecurity集成JWT
(1) 認(rèn)證配置
a) 配置SpringSecurity
首先配置一個SpringSecurity的配置類, 因為是基于JWT進(jìn)行認(rèn)證, 所以需要在配置中禁用session機(jī)制, 并不是禁用整個系統(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()
// 指定某些接口不需要通過驗證即可訪問。像登陸、注冊接口肯定是不需要認(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) 實現(xiàn)登錄接口
先按照正常流程, 實現(xiàn)一個登錄的接口然后在業(yè)務(wù)層中實現(xiàn)
@PostMapping("/login")
public Res login(@RequestBody User user, HttpServletRequest request) {
return userService.login(user, request);
}
在業(yè)務(wù)層中, 首先對密碼和用戶名進(jìn)行檢驗, 然后更新security登錄用戶對象, 在此之前我們先來認(rèn)識幾個在SpringSecurity中重要的變量
Authentication: 存儲了認(rèn)證信息, 代表登錄用戶SecurityContext: 上下文對象, 用來獲取Authentication(用戶信息)SecurityContextHolder: 上下文管理對象, 用來在程序任何地方獲取SecurityContextUserDetails: 存儲了用戶的基本信息, 以及用戶權(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();
// 登陸 檢測
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ù)據(jù)庫或者緩存中查詢用戶提交的用戶名以及用戶的權(quán)限信息, 將這些信息保存在userDetails中
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken 實現(xiàn)了Authentication, 也就是說此時將userDetails中的信息以及權(quán)限信息存放在Authentication中
創(chuàng)建Token需要JWT的工具類, 在網(wǎng)上隨便找個都可以, 大致都一樣, 這個只需要知道就行了
c) 過濾請求
在原生SpringSecurity中默認(rèn)的攔截在UsernamePasswordAuthenticationFilter這個類中,該類主要攔截表單提交的用戶名和密碼, 顯然在前后端分離項目中不適用, 而且我們用到了JWT的驗證方式, 前端每次請求都需要帶上token, 所以我們需要在后端對每個請求進(jìn)行提前過濾攔截
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() 獲取上下文對象中認(rèn)證信息
if(null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
// 自定義數(shù)據(jù)源獲取用戶信息
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 驗證token是否有效 驗證token用戶名和存儲的用戶名是否一致以及是否在有效期內(nèi), 重新設(shè)置用戶對象
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ā)送的請求頭信息, 根據(jù)JWT的工具類獲取用戶名
- 如果請求頭具有有效的字符串(也就是擁有用戶信息)并且上下文對象存在用戶信息(數(shù)據(jù)庫或者緩存中查的用戶信息)則直接到下一個過濾器, 否則請求頭中有信息而當(dāng)前上下文對象沒有存儲用戶信息則將請求頭中的用戶在數(shù)據(jù)層驗證之后重新放入上下文對象中(UsernamePasswordAuthenticationToken)。
- 如果當(dāng)前用戶沒有登錄或者沒有token信息(可能是token過期), 而當(dāng)前請求的地址符合權(quán)限中包含的地址(也就是數(shù)據(jù)庫中存在的), 則會進(jìn)入權(quán)限驗證(下面會講)
當(dāng)然以上的邏輯可以自己自定義, 不管以上什么情況都會進(jìn)入權(quán)限驗證
要讓這個過濾器加入到SpringSecurity的過濾器鏈中, 就需要在SecurityConfig類的configure方法添加下面一條語句, addFilterBefore()將jwtAuthencationTokenFilter(), 放在UsernamePasswordAuthenticationFilter之前
http.addFilterBefore(jwtAuthencationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
(2) 權(quán)限配置
在一個項目中, 不同的用戶需要具有不同的權(quán)限, 我們怎么對用戶進(jìn)行區(qū)分呢?
a) RBAC權(quán)限表
將用戶、角色和權(quán)限綁定,這樣可以知道某個用戶具有哪些角色, 而某個角色對應(yīng)有哪些權(quán)限(能干什么,不能干什么),這樣就知道哪些用戶擁有的角色和權(quán)限信息。
基于以上的想法, 我們需要三張實體表, 還需要兩張多對多的關(guān)系表, 這樣就構(gòu)成了RBAC的五張表
b) 授權(quán)流程
在SpringSecurity中授權(quán)的過濾器是FilterSecurityInterceptor
默認(rèn)的流程
- 調(diào)用
SecurityMetadataSource獲取當(dāng)前請求的鑒權(quán)規(guī)則 - 接著調(diào)用
AccessDecisionManager來校驗當(dāng)前用戶的是否擁有當(dāng)前權(quán)限 - 如果有權(quán)限就放行, 否則拋出異常, 該異常則會被
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("權(quán)限不足, 請聯(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)信息, 可以從中獲取請求的方法和URL等信息
然后menus得到的是當(dāng)前數(shù)據(jù)層中所有的權(quán)限路徑, 接著循環(huán)所有的路徑信息與當(dāng)前請求的方法和URL進(jìn)行驗證, 如果在數(shù)據(jù)層中沒有當(dāng)前請求則返回null, 否則將該權(quán)限的在數(shù)據(jù)層中的信息返回
c) 自定義AccessDecisionManager
如果在SecurityMetadataSource中有權(quán)限信息, 則會進(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("尚未登錄, 請登錄");
}
if(Objects.equals(authority.getAuthority(), configAttribute.getAttribute())) {
return;
}
}
}
throw new AccessDeniedException("權(quán)限不足, 請聯(lián)系管理員!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
首先來看幾個變量
- ConfigAttribute: 這個是鑒權(quán)的規(guī)則, 根據(jù)自己項目設(shè)定, 我們這里填入的是當(dāng)前請求和數(shù)據(jù)層中相匹配的權(quán)限信息id
- GrantedAuthority: 當(dāng)前認(rèn)證用戶所擁有的權(quán)限信息
在上述的decide方法中, 主要驗證了用戶所擁有的權(quán)限和當(dā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)限不足, 請聯(lián)系管理員!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
e) 在SpringSecurity中的配置
在configure方法中, 進(jìn)行了動態(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;
}
})
插入: 還有一個認(rèn)證異常處理
- 用戶首次登錄且驗證成功, 此時正常用戶權(quán)限授權(quán)
- 請求數(shù)據(jù)時, 非首次登錄, 如果沒有攜帶token(token過期), 又或者沒有登錄訪問內(nèi)部路徑時, 說明沒有認(rèn)證權(quán)限不能訪問, 拋出未登錄異常
- 請求數(shù)據(jù)時, 有token信息, 而上下文對象中沒有用戶信息, 則會重新將用戶信息放入上下文對象中, 接著進(jìn)入權(quán)限驗證, 如果用戶擁有該權(quán)限則放行, 如果沒有該權(quán)限則拋出權(quán)限不足異常
在configure中配置未登錄和未授權(quán)異常處理
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthoricationEntryPoint);
四. 總結(jié)
其實以上配置還有很多漏洞, 比如token的過期時間, 當(dāng)用戶上一秒還在請求數(shù)據(jù), 下一秒token過期, 則會造成用戶需要重新登錄, 顯然不合適
這是項目的地址 Github下載
到此這篇關(guān)于Spring Security+JWT簡述的文章就介紹到這了,更多相關(guān)Spring Security JWT簡述內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringSecurity 手機(jī)號登錄功能實現(xiàn)
這篇文章主要介紹了SpringSecurity 手機(jī)號登錄功能實現(xiàn),本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2023-12-12
springboot攔截器Interceptor的使用,你都了解嗎
springmvc 中的攔截器可以對請求進(jìn)行判別,在請求到達(dá)控制器之前,把非法的請求給攔截掉下面來說一說, 它在springboot中的使用,感興趣的朋友一起看看吧2021-07-07
關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性
這篇文章主要介紹了關(guān)于Spring中的@Configuration中的proxyBeanMethods屬性,需要的朋友可以參考下2023-07-07

