SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
一、SpringSecurity簡介
Spring Security 是 Spring 家族中的一個(gè)安全管理框架。相比與另外一個(gè)安全框架Shiro,它提供了更豐富的功能,社區(qū)資源也比Shiro豐富。
一般來說中大型的項(xiàng)目都是使用SpringSecurity 來做安全框架。小項(xiàng)目有Shiro的比較多,因?yàn)橄啾扰cSpringSecurity,Shiro的上手更加的簡單。
一般Web應(yīng)用的需要進(jìn)行認(rèn)證和授權(quán)。
- 認(rèn)證:驗(yàn)證當(dāng)前訪問系統(tǒng)的是不是本系統(tǒng)的用戶,并且要確認(rèn)具體是哪個(gè)用戶
- 授權(quán):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個(gè)操作
而認(rèn)證和授權(quán)也是SpringSecurity作為安全框架的核心功能。
1.1 入門Demo
依賴如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
引入依賴后我們在嘗試去訪問之前的接口就會自動跳轉(zhuǎn)到一個(gè)SpringSecurity的默認(rèn)登陸頁面,默認(rèn)用戶名是user,密碼會輸出在控制臺。
必須登陸之后才能對接口進(jìn)行訪問。
訪問 localhost:8080/logout 這個(gè)鏈接可以 對其進(jìn)行退出操作。
Ps:
以上過程了解即可,因?yàn)槲覀儗?shí)際Web項(xiàng)目中,一般采用我們自定義的登錄驗(yàn)證授權(quán)方案,不會采取SpringSecurity框架提供的默認(rèn)方案。
二、認(rèn)證
登錄校驗(yàn)流程:
為了實(shí)現(xiàn)以上這種過程,我們需要先對SpringSecurity默認(rèn)的流程進(jìn)行了解,才可以對其進(jìn)行修改,實(shí)現(xiàn)我們自定義的方案。
2.1 SpringSecurity完整流程
SpringSecurity的原理其實(shí)就是一個(gè)過濾器鏈,內(nèi)部包含了提供各種功能的過濾器。這里我們可以看看入門案例中的過濾器:
圖中只展示了核心過濾器,其它的非核心過濾器并沒有在圖中展示:
- UsernamePasswordAuthenticationFilter:負(fù)責(zé)處理我們在登陸頁面填寫了用戶名密碼后的登陸請求。入門案例的認(rèn)證工作主要有它負(fù)責(zé)。
- ExceptionTranslationFilter:處理過濾器鏈中拋出的任何AccessDeniedException和AuthenticationException 。
- FilterSecurityInterceptor:負(fù)責(zé)權(quán)限校驗(yàn)的過濾器。我們可以通過Debug查看當(dāng)前系統(tǒng)中SpringSecurity過濾器鏈中有哪些過濾器及它們的順序。
如果想查看所有的過濾器,可以通過獲取Spring容器,Debug方式來查看:
2.2 認(rèn)證流程詳解
箭頭代表該方法屬于這個(gè)實(shí)現(xiàn)類的。
概念速查:
- Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
- AuthenticationManager接口:定義了認(rèn)證Authentication的方法
- UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個(gè)根據(jù)用戶名查詢用戶信息的方法。
- UserDetails接口:提供核心用戶信息。通過UserDetailsService根據(jù)用戶名獲取處理的用戶信息要封裝成UserDetails對象返回。然后將這些信息封裝到Authentication對象中。
2.3 自定義認(rèn)證實(shí)現(xiàn)
登錄
①自定義登錄接口
調(diào)用ProviderManager的方法進(jìn)行認(rèn)證 如果認(rèn)證通過生成jwt 把用戶信息存入redis中
②自定義UserDetailsService
在這個(gè)實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫
校驗(yàn)
①定義Jwt 認(rèn)證過濾器
獲取token 解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder
這里為什么要存入 SecurityContextHolder中呢?
我們自定義的JWT過濾器的時(shí)候,肯定是需要將這個(gè)JWT過濾器放在UsernamePasswordAuthenticationFilter前的,這時(shí)我們將從redis獲取的用戶信息存入SecurityContextHolder才行,否則后續(xù)過濾器在進(jìn)行校驗(yàn)的時(shí)候,可能會因?yàn)镾ecurityContextHolder中沒有對應(yīng)的值而判斷當(dāng)前訪問用戶驗(yàn)證不通過。
2.3.1 數(shù)據(jù)庫校驗(yàn)用戶
定義Mapper接口
package com.example.springsecurity_demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.springsecurity_demo.domain.User; public interface UserMapper extends BaseMapper<User> { }
定義User實(shí)體類
package com.example.springsecurity_demo.domain; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; @Data @AllArgsConstructor @NoArgsConstructor @TableName("sys_user") public class User implements Serializable { private static final long serialVersionUID = -40356785423868312L; /** * 主鍵 */ @TableId private Long id; /** * 用戶名 */ private String userName; /** * 昵稱 */ private String nickName; /** * 密碼 */ private String password; /** * 賬號狀態(tài)(0正常 1停用) */ private String status; /** * 郵箱 */ private String email; /** * 手機(jī)號 */ private String phonenumber; /** * 用戶性別(0男,1女,2未知) */ private String sex; /** * 頭像 */ private String avatar; /** * 用戶類型(0管理員,1普通用戶) */ private String userType; /** * 創(chuàng)建人的用戶id */ private Long createBy; /** * 創(chuàng)建時(shí)間 */ private Date createTime; /** * 更新人 */ private Long updateBy; /** * 更新時(shí)間 */ private Date updateTime; /** * 刪除標(biāo)志(0代表未刪除,1代表已刪除) */ private Integer delFlag; }
配置Mapper掃描
package com.example.springsecurity_demo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @SpringBootApplication @MapperScan("com.example.springsecurity_demo.mapper") public class SpringSecurityDemoApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(SpringSecurityDemoApplication.class, args); System.out.println(1); } }
核心代碼實(shí)現(xiàn)
package com.example.springsecurity_demo.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.springsecurity_demo.domain.LoginUser; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.Objects; @Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 查詢用戶信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果沒有查詢到用戶就拋出異常 if (Objects.isNull(user)) { throw new RuntimeException("用戶名或者密碼錯(cuò)誤"); } //TODO 查詢對應(yīng)的權(quán)限信息 return new LoginUser(user); } }
因?yàn)閁serDetailsService方法的返回值是UserDetails(接口):
所以需要定義一個(gè)類,實(shí)現(xiàn)該接口,把用戶信息封裝在其中。
package com.example.springsecurity_demo.domain; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; @Data @NoArgsConstructor @AllArgsConstructor public class LoginUser implements UserDetails { private User user; @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
注意:如果要測試,需要往用戶表中寫入用戶數(shù)據(jù),并且如果你想讓用戶的密碼是明文存儲,需要在密碼前加{noop}。例如:
這樣登陸的時(shí)候就可以用fox作為用戶名,123作為密碼來登陸了。
2.3.2 密碼加密存儲
實(shí)際項(xiàng)目中我們不會把密碼明文存儲在數(shù)據(jù)庫中。
默認(rèn)使用的PasswordEncoder要求數(shù)據(jù)庫中的密碼格式為:{id}password 。它會根據(jù)id去判斷密碼的加密方式。但是我們一般不會采用這種方式。所以就需要替換PasswordEncoder。
我們一般使用SpringSecurity為我們提供的BCryptPasswordEncoder。
我們只需要使用把BCryptPasswordEncoder對象注入Spring容器中,SpringSecurity就會使用該
PasswordEncoder來進(jìn)行密碼校驗(yàn)。
我們可以定義一個(gè)SpringSecurity的配置類:
低版本配置如下:
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
以下是高版本的SpringSecurity(SpringBoot 3 用以下配置):
package com.example.springsecurity_demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
2.3.3 登錄接口實(shí)現(xiàn)
接下來我們需要自定義登錄接口,這里我們需要讓SpringSecurity對這個(gè)接口放行,讓用戶訪問這個(gè)接口的時(shí)候不用登錄也能訪問。(畢竟登錄接口如果還需要權(quán)限訪問,那么就很奇怪了)
在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。
認(rèn)證成功的話要生成一個(gè)JWT,放入響應(yīng)中返回,并且為了讓用戶下回請求時(shí)需要通過JWT識別出具體的是哪個(gè)用戶,我們需要把用戶信息存入redis,可以把用戶id作為key。
Contorller類如下:
package com.example.springsecurity_demo.controller; import com.example.springsecurity_demo.domain.ResponseResult; import com.example.springsecurity_demo.domain.User; import com.example.springsecurity_demo.service.LoginService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class LoginController { @Autowired private LoginService loginService; @PostMapping("/user/login") public ResponseResult login(@RequestBody User user){ return loginService.login(user); } }
Ps:
雖然字段聲明的類型是 LoginService,但實(shí)際上注入的是 LoginServiceImpl。這是因?yàn)?LoginServiceImpl 實(shí)現(xiàn)了 LoginService 接口,因此它被視為 LoginService 的一種類型。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); // } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
實(shí)現(xiàn)類如下:
import com.sangeng.domain.LoginUser; import com.sangeng.domain.ResponseResult; import com.sangeng.domain.User; import com.sangeng.service.LoginServcie; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { //AuthenticationManager authenticate進(jìn)行用戶認(rèn)證 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); //如果認(rèn)證沒通過,給出對應(yīng)的提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("登錄失敗"); } //如果認(rèn)證通過了,使用userid生成一個(gè)jwt jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userid = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userid); Map<String,String> map = new HashMap<>(); map.put("token",jwt); //把完整的用戶信息存入redis userid作為key redisCache.setCacheObject("login:"+userid,loginUser); return new ResponseResult(200,"登錄成功",map); } }
分析:
這里創(chuàng)建UsernamePasswordAuthenticationToken對象是因?yàn)檎{(diào)用authenticationManager.authenticate方法 需要傳入Authentication,但是Authentication又是一個(gè)接口,所以需要傳入其實(shí)現(xiàn)類。
2.3.4 認(rèn)證過濾器
我們需要自定義一個(gè)過濾器,這個(gè)過濾器會去獲取請求頭中的token,對token進(jìn)行解析取出其中的
userid。
使用userid去redis中獲取對應(yīng)的LoginUser對象。
然后封裝Authentication對象存入SecurityContextHolder。
import com.sangeng.domain.LoginUser; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Objects; @Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisCache redisCache; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取token String token = request.getHeader("token"); if (!StringUtils.hasText(token)) { //放行 filterChain.doFilter(request, response); return; } //解析token String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("token非法"); } //從redis中獲取用戶信息 String redisKey = "login:" + userid; LoginUser loginUser = redisCache.getCacheObject(redisKey); if(Objects.isNull(loginUser)){ throw new RuntimeException("用戶未登錄"); } //存入SecurityContextHolder //TODO 獲取權(quán)限信息封裝到Authentication中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); //放行 filterChain.doFilter(request, response); } }
分析:
這里先設(shè)置放行最后再使用return是因?yàn)?,如果該請求無含token,那么就對其進(jìn)行放行,讓請求進(jìn)入下一個(gè)攔截器,后續(xù)Security框架還有很多攔截器可以對其進(jìn)行驗(yàn)證,而使用return是因?yàn)楹罄m(xù)在進(jìn)行參數(shù)返回的時(shí)候,不需要再執(zhí)行以下代碼。
簡單來說:圖中代碼紅線以上部分是參數(shù)請求時(shí)候所執(zhí)行的部分,紅線以下是返回響應(yīng)體時(shí)候所執(zhí)行的部分。
還有個(gè)細(xì)節(jié)是:這里必須調(diào)用三個(gè)參數(shù)的構(gòu)造方法,而不是兩個(gè),這是因?yàn)橹挥姓{(diào)用三個(gè)構(gòu)造方法的時(shí)候,才能保證該主體是認(rèn)證過的,否則框架檢查時(shí)候還是會報(bào)錯(cuò):
SecurityConfig如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認(rèn)證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
2.3.5 退出登錄
我們只需要定義一個(gè)登陸接口,然后獲取SecurityContextHolder中的認(rèn)證信息,刪除redis中對應(yīng)的數(shù)據(jù)即可。
import com.sangeng.domain.LoginUser; import com.sangeng.domain.ResponseResult; import com.sangeng.domain.User; import com.sangeng.service.LoginServcie; import com.sangeng.utils.JwtUtil; import com.sangeng.utils.RedisCache; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; import java.util.Objects; @Service public class LoginServiceImpl implements LoginServcie { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult logout() { //獲取SecurityContextHolder中的用戶id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); //刪除redis中的值 redisCache.deleteObject("login:"+userid); return new ResponseResult(200,"注銷成功"); } }
分析:
這里并不需要刪除SecurityContextHolder中的信息,只需要刪除redis中所存儲的即可,因?yàn)樵谶M(jìn)行認(rèn)證的時(shí)候,需要先在SecurityContextHolder中拿到信息后,再從redis中獲取對應(yīng)信息。
每個(gè)請求都對應(yīng)一個(gè)SecurityContextHolder,所以刪除SecurityContextHolder中的信息是無效的,需要刪除redis中所存儲的信息。
三、授權(quán)
3.1 權(quán)限系統(tǒng)作用
例如一個(gè)學(xué)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個(gè)圖書館管理員的賬號登錄了,應(yīng)該就能看到并使用添加書籍信息,刪除書籍信息等功能。
總結(jié)起來就是不同的用戶可以使用不同的功能。這就是權(quán)限系統(tǒng)要去實(shí)現(xiàn)的效果。
我們不能只依賴前端去判斷用戶的權(quán)限來選擇顯示哪些菜單哪些按鈕。因?yàn)槿绻皇沁@樣,如果有人知道了對應(yīng)功能的接口地址就可以不通過前端,直接去發(fā)送請求來實(shí)現(xiàn)相關(guān)功能操作。
所以我們還需要在后臺進(jìn)行用戶權(quán)限的判斷,判斷當(dāng)前用戶是否有相應(yīng)的權(quán)限,必須具有所需權(quán)限才能進(jìn)行相應(yīng)的操作。
3.2 授權(quán)基本流程
在SpringSecurity中,會使用默認(rèn)的FilterSecurityInterceptor來進(jìn)行權(quán)限校驗(yàn)。在
FilterSecurityInterceptor中會從SecurityContextHolder獲取其中的Authentication,然后獲取其中的
權(quán)限信息。當(dāng)前用戶是否擁有訪問當(dāng)前資源所需的權(quán)限。
所以我們在項(xiàng)目中只需要把當(dāng)前登錄用戶的權(quán)限信息也存入Authentication。
然后設(shè)置我們的資源所需要的權(quán)限即可。
如何將權(quán)限信息存入Authentication呢?
回顧之前的代碼,我們需要在自定義的認(rèn)證過濾器中,將Authentication存入SecurityContextHolder中,那么Authentication的信息又是從哪里來呢?
:是從redis中獲取到的。
也就是我們當(dāng)初應(yīng)該往redis中存入權(quán)限信息,而redis中存儲的是loginUser,
loginUser是當(dāng)初我們UserDetail的自定義實(shí)現(xiàn)類中所查詢到的用戶信息(也就是:從數(shù)據(jù)庫中查詢的)。
而最終這個(gè)loginUser對象會返回給這個(gè)authenticate對象,那么authenticate這個(gè)對象就會存入redis中。
以上就是整個(gè)流程,所以我們最終只需要完成兩個(gè)步驟即可:
- 在查詢數(shù)據(jù)庫的時(shí)候獲取對應(yīng)的權(quán)限信息。
- 在實(shí)現(xiàn)認(rèn)證過濾器的時(shí)候,需要獲取當(dāng)前用戶的權(quán)限信息,并存入到SecurityContextHolder中。
也就是需要完善上述圖片中兩個(gè) TODO 標(biāo)識的代碼片段。
3.3 授權(quán)實(shí)現(xiàn)
3.3.1 限制訪問資源所需權(quán)限
SpringSecurity為我們提供了基于注解的權(quán)限控制方案,這也是我們項(xiàng)目中主要采用的方式。我們可以使用注解去指定訪問對應(yīng)的資源所需的權(quán)限。
但是要使用它我們需要先開啟相關(guān)配置,即在關(guān)于SpringSecurity的配置類中添加以下代碼:
@EnableGlobalMethodSecurity(prePostEnabled = true)
設(shè)置完之后,就可以使用對應(yīng)的注解:@PreAuthorize。
@RestController public class HelloController { @RequestMapping("/hello") @PreAuthorize("hasAuthority('test')") public String hello(){ return "hello"; } }
分析:
這里的test只供測試,test使用單引號是因?yàn)橥鈱右呀?jīng)有了雙引號,所以使用單引號來標(biāo)識這是個(gè)字符串,實(shí)際上我們通過idea的提示(ctrl+p),也可以知道,這個(gè)hasAuthority所需要的參數(shù)也是String類型:
3.3.2 封裝權(quán)限信息
我們前面在寫UserDetailsServiceImpl的時(shí)候說過,在查詢出用戶后還要獲取對應(yīng)的權(quán)限信息,封裝到UserDetails中返回。
我們先直接把權(quán)限信息寫死封裝到UserDetails中進(jìn)行測試。
我們之前定義了UserDetails的實(shí)現(xiàn)類LoginUser,想要讓其能封裝權(quán)限信息就要對其進(jìn)行修改。
import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; @Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; private List<String> permissions; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; @Override public Collection<? extends GrantedAuthority> getAuthorities() { if(authorities!=null){ return authorities; } //把permissions中String類型的權(quán)限信息封裝成SimpleGrantedAuthority對象 authorities = new ArrayList<>(); for (String permission : permissions) { SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission); authorities.add(authority); } // authorities = permissions.stream() // .map(SimpleGrantedAuthority::new) // .collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
分析:
這里我們重寫了getAuthorities方法, permission這個(gè)屬性存儲的是權(quán)限信息。
在getAuthorities方法中,由于返回值是一個(gè)Collection類型,所以我們這里選擇List集合。
又因?yàn)榉盒鸵笫荊rantedAuthority的子類,但是其是一個(gè)接口,所以我們通過查找其實(shí)現(xiàn)類(ctrl+alt+鼠標(biāo)左鍵):
選擇了SimpleGrantedAuthority,因?yàn)榭此钟袀€(gè)simple,再觀察其構(gòu)造方法,發(fā)現(xiàn)只需要傳入一個(gè)字符串即可,于是我們就需要將我們類屬性的permission全部都通過構(gòu)造方法存入SimpleGrantedAuthority屬性中,然后將其一個(gè)個(gè)遍歷,放入list集合中。
還有一個(gè)小細(xì)節(jié)是這里加了@JSONField(serialize = false) 注解防止redis存儲loginUser時(shí)候序列化出錯(cuò)(報(bào)異常),因?yàn)镾impleGrantedAuthority是Spring中提供的類。
雖然我們這里不讓這個(gè)List集合序列化,但是并不影響后續(xù)操作,因?yàn)樵谌〕鰜矸葱蛄谢臅r(shí)候,我們自定義的permission屬性是可以被正常序列化的,那個(gè)時(shí)候通過它就可以讓程序正常運(yùn)行。
3.3.3 從數(shù)據(jù)庫查詢權(quán)限信息
3.3.3.1 RBAC權(quán)限模型
RBAC權(quán)限模型(Role-Based Access Control)即:基于角色的權(quán)限控制。這是目前最常被開發(fā)者使用也是相對易用、通用權(quán)限模型。
3.3.3.2 代碼實(shí)現(xiàn)
Menu類如下:
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.Date; /** * 菜單表(Menu)實(shí)體類 * * @author makejava * @since 2021-11-24 15:30:08 */ @TableName(value="sys_menu") @Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(JsonInclude.Include.NON_NULL) public class Menu implements Serializable { private static final long serialVersionUID = -54979041104113736L; @TableId private Long id; /** * 菜單名 */ private String menuName; /** * 路由地址 */ private String path; /** * 組件路徑 */ private String component; /** * 菜單狀態(tài)(0顯示 1隱藏) */ private String visible; /** * 菜單狀態(tài)(0正常 1停用) */ private String status; /** * 權(quán)限標(biāo)識 */ private String perms; /** * 菜單圖標(biāo) */ private String icon; private Long createBy; private Date createTime; private Long updateBy; private Date updateTime; /** * 是否刪除(0未刪除 1已刪除) */ private Integer delFlag; /** * 備注 */ private String remark; }
mapper如下:
我們只需要根據(jù)用戶id去查詢到其所對應(yīng)的權(quán)限信息即可。
所以我們可以先定義個(gè)mapper,其中提供一個(gè)方法可以根據(jù)userid查詢權(quán)限信息。
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.sangeng.domain.Menu; import java.util.List; public interface MenuMapper extends BaseMapper<Menu> { List<String> selectPermsByUserId(Long userid); }
尤其是自定義方法,所以需要創(chuàng)建對應(yīng)的mapper文件,定義對應(yīng)的sql語句
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.fox.mapper.MenuMapper"> <select id="selectPermsByUserId" resultType="java.lang.String"> SELECT DISTINCT m.`perms` FROM sys_user_role ur LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE user_id = #{userid} AND r.`status` = 0 AND m.`status` = 0 </select> </mapper>
然后我們可以在UserDetailsServiceImpl中去調(diào)用該mapper的方法查詢權(quán)限信息封裝到LoginUser對象中即可:
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.sangeng.domain.LoginUser; import com.sangeng.domain.User; import com.sangeng.mapper.MenuMapper; import com.sangeng.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //查詢用戶信息 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); User user = userMapper.selectOne(queryWrapper); // 如果沒有查詢到用戶就拋出異常 if(Objects.isNull(user)){ throw new RuntimeException("用戶名或者密碼錯(cuò)誤"); } // List<String> list = new ArrayList<>(Arrays.asList("test","admin")); List<String> list = menuMapper.selectPermsByUserId(user.getId()); //把數(shù)據(jù)封裝成UserDetails返回 return new LoginUser(user,list); } }
四、自定義失敗處理
我們還希望在認(rèn)證失敗或者是授權(quán)失敗的情況下也能和我們的接口一樣返回相同結(jié)構(gòu)的json,這樣可以讓前端能對響應(yīng)進(jìn)行統(tǒng)一的處理。要實(shí)現(xiàn)這個(gè)功能我們需要知道SpringSecurity的異常處理機(jī)制。
在SpringSecurity中,如果我們在認(rèn)證或者授權(quán)的過程中出現(xiàn)了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslationFilter中會去判斷是認(rèn)證失敗還是授權(quán)失敗出現(xiàn)的異常。
如果是認(rèn)證過程中出現(xiàn)的異常會被封裝成AuthenticationException然后調(diào)用
AuthenticationEntryPoint對象的方法去進(jìn)行異常處理。
如果是授權(quán)過程中出現(xiàn)的異常會被封裝成AccessDeniedException然后調(diào)用AccessDeniedHandler對象的方法去進(jìn)行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和
AccessDeniedHandler然后配置給SpringSecurity即可。
4.1 創(chuàng)建自定義實(shí)現(xiàn)類
也就是說,我們只需要創(chuàng)建一個(gè)自定義的實(shí)現(xiàn)類然后分別去實(shí)現(xiàn)AccessDeniedHandler接口和AccessDeniedHandler接口即可,代碼如下。
認(rèn)證失敗自定義實(shí)現(xiàn)類如下:
import com.alibaba.fastjson.JSON; import com.sangeng.domain.ResponseResult; import com.sangeng.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用戶認(rèn)證失敗請查詢登錄"); String json = JSON.toJSONString(result); //處理異常 WebUtils.renderString(response,json); } }
授權(quán)失敗自定義實(shí)現(xiàn)類如下:
import com.alibaba.fastjson.JSON; import com.sangeng.domain.ResponseResult; import com.sangeng.utils.WebUtils; import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(),"您的權(quán)限不足"); String json = JSON.toJSONString(result); //處理異常 WebUtils.renderString(response,json); } }
涉及到的工具類如下:
由于response對象是較為原生的,所以我們需要進(jìn)行書寫狀態(tài)碼,ContentType等。所以我們需要使用工具類對其進(jìn)行修改。
import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class WebUtils { /** * 將字符串渲染到客戶端 * * @param response 渲染對象 * @param string 待渲染的字符串 * @return null */ public static String renderString(HttpServletResponse response, String string) { try { response.setStatus(200); response.setContentType("application/json"); response.setCharacterEncoding("utf-8"); response.getWriter().print(string); } catch (IOException e) { e.printStackTrace(); } return null; } }
4.2 將實(shí)現(xiàn)類配置給SpringSecurity
注入對應(yīng)處理器:
然后我們可以使用HttpSecurity對象的方法去配置:
配合類代碼如下:
import com.sangeng.filter.JwtAuthenticationTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { //創(chuàng)建BCryptPasswordEncoder注入容器 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private AccessDeniedHandler accessDeniedHandler; @Override protected void configure(HttpSecurity http) throws Exception { http //關(guān)閉csrf .csrf().disable() //不通過Session獲取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 對于登錄接口 允許匿名訪問 .antMatchers("/user/login").anonymous() // .antMatchers("/testCors").hasAuthority("system:dept:list222") // 除上面外的所有請求全部需要鑒權(quán)認(rèn)證 .anyRequest().authenticated(); //添加過濾器 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); //配置異常處理器 http.exceptionHandling() //配置認(rèn)證失敗處理器 .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); //允許跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
五、跨域問題解決方案
瀏覽器出于安全的考慮,使用 XMLHttpRequest對象發(fā)起 HTTP請求時(shí)必須遵守同源策略,否則就是跨域的HTTP請求,默認(rèn)情況下是被禁止的。 同源策略要求源相同才能正常進(jìn)行通信,即協(xié)議、域名、端口號都完全一致。
前后端分離項(xiàng)目,前端項(xiàng)目和后端項(xiàng)目一般都不是同源的,所以肯定會存在跨域請求的問題。
所以我們就要處理一下,讓前端能進(jìn)行跨域請求。
①先對SpringBoot配置,運(yùn)行跨域請求
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 設(shè)置允許跨域的路徑 registry.addMapping("/**") // 設(shè)置允許跨域請求的域名 .allowedOriginPatterns("*") // 是否允許cookie .allowCredentials(true) // 設(shè)置允許的請求方式 .allowedMethods("GET", "POST", "DELETE", "PUT") // 設(shè)置允許的header屬性 .allowedHeaders("*") // 跨域允許時(shí)間 .maxAge(3600); } }
②開啟SpringSecurity的跨域訪問
由于我們的資源都會收到SpringSecurity的保護(hù),所以想要跨域訪問還要讓SpringSecurity運(yùn)行跨域訪問。
六、其他權(quán)限校驗(yàn)方法
我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進(jìn)行校驗(yàn)。
SpringSecurity還為我們提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等。
這里我們先不急著去介紹這些方法,我們先去理解hasAuthority的原理,然后再去學(xué)習(xí)其他方法你就更容易理解,而不是死記硬背區(qū)別。并且我們也可以選擇定義校驗(yàn)方法,實(shí)現(xiàn)我們自己的校驗(yàn)邏輯。
hasAuthority方法實(shí)際是執(zhí)行到了SecurityExpressionRoot的hasAuthority,大家只要斷點(diǎn)調(diào)試既可知道它內(nèi)部的校驗(yàn)原理。
它內(nèi)部其實(shí)是調(diào)用authentication的getAuthorities方法獲取用戶的權(quán)限列表。然后判斷我們存入的方法參數(shù)數(shù)據(jù)在權(quán)限列表中。
hasAnyAuthority方法可以傳入多個(gè)權(quán)限,只有用戶有其中任意一個(gè)權(quán)限都可以訪問對應(yīng)資源。
@RequestMapping("/hello") @PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')") public String hello(){ return "hello"; }
hasRole要求有對應(yīng)的角色才可以訪問,但是它內(nèi)部會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
@RequestMapping("/hello") @PreAuthorize("hasRole('system:dept:list')") public String hello(){ return "hello"; }
hasAnyRole 有任意的角色就可以訪問。它內(nèi)部也會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應(yīng)的權(quán)限也要有 ROLE_ 這個(gè)前綴才可以。
@RequestMapping("/hello") @PreAuthorize("hasAnyRole('admin','system:dept:list')") public String hello(){ return "hello"; }
七、自定義權(quán)限校驗(yàn)方法
我們也可以定義自己的權(quán)限校驗(yàn)方法,在@PreAuthorize注解中使用我們的方法。
import com.fox.domain.LoginUser; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.List; @Component("ex") public class FoxExpressionRoot { public boolean hasAuthority(String authority){ //獲取當(dāng)前用戶的權(quán)限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); //判斷用戶權(quán)限集合中是否存在authority return permissions.contains(authority); } }
在SPEL表達(dá)式中使用 @ex相當(dāng)于獲取容器中bean的名字未ex的對象。然后再調(diào)用這個(gè)對象的
hasAuthority方法:
@RequestMapping("/hello") @PreAuthorize("@ex.hasAuthority('system:dept:list')") public String hello(){ return "hello"; }
八、基于配置的權(quán)限控制
我們也可以在配置類中使用使用配置的方式對資源進(jìn)行權(quán)限控制。
到此這篇關(guān)于SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼的文章就介紹到這了,更多相關(guān)SpringSecurity 認(rèn)證與授權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過程
- SpringBoot整合SpringSecurity和JWT和Redis實(shí)現(xiàn)統(tǒng)一鑒權(quán)認(rèn)證
- SpringBoot整合SpringSecurityOauth2實(shí)現(xiàn)鑒權(quán)動態(tài)權(quán)限問題
- SpringBoot集成SpringSecurity和JWT做登陸鑒權(quán)的實(shí)現(xiàn)
- SpringSecurity動態(tài)加載用戶角色權(quán)限實(shí)現(xiàn)登錄及鑒權(quán)功能
- springboot+jwt+springSecurity微信小程序授權(quán)登錄問題
- SpringSecurity實(shí)現(xiàn)權(quán)限認(rèn)證與授權(quán)的使用示例
- springSecurity用戶認(rèn)證和授權(quán)的實(shí)現(xiàn)
- 深入淺析springsecurity入門登錄授權(quán)
- mall整合SpringSecurity及JWT實(shí)現(xiàn)認(rèn)證授權(quán)實(shí)戰(zhàn)
- SpringSecurity頁面授權(quán)與登錄驗(yàn)證實(shí)現(xiàn)(內(nèi)存取值與數(shù)據(jù)庫取值)
- SpringSecurity 鑒權(quán)與授權(quán)的具體使用
相關(guān)文章
Java Arrays.sort和Collections.sort排序?qū)崿F(xiàn)原理解析
這篇文章主要介紹了Java Arrays.sort和Collections.sort排序?qū)崿F(xiàn)原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解
這篇文章主要介紹了SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解,Hystrix是一個(gè)用于處理分布式系統(tǒng)的延遲和容錯(cuò)的開源庫,在分布式系統(tǒng)里,許多依賴不可避免的會調(diào)用失敗,比如超時(shí)、異常等,需要的朋友可以參考下2024-01-01SpringBoot 利用thymeleaf自定義錯(cuò)誤頁面
這篇文章主要介紹了SpringBoot 利用thymeleaf自定義錯(cuò)誤頁面,幫助大家更好的理解和使用springboot 框架,感興趣的朋友可以了解下2020-11-11elasticsearch如何根據(jù)條件刪除數(shù)據(jù)
Elasticsearch是一個(gè)基于Apache Lucene?的開源搜索引擎,無論在開源還是專有領(lǐng)域,Lucene 可以被認(rèn)為是迄今為止最先進(jìn)、性能最好的、功能最全的搜索引擎庫,這篇文章主要介紹了elasticsearch如何根據(jù)條件刪除數(shù)據(jù),需要的朋友可以參考下2023-03-03Java的JDBC編程使用之連接Mysql數(shù)據(jù)庫
這篇文章主要給大家介紹了關(guān)于Java的JDBC編程使用之連接Mysql數(shù)據(jù)庫的相關(guān)資料,JDBC是一種用于執(zhí)行SQL語句的Java?API,可以為多種關(guān)系數(shù)據(jù)庫提供統(tǒng)一訪問,需要的朋友可以參考下2023-12-12TransmittableThreadLocal線程間傳遞邏輯示例解析
這篇文章主要介紹了TransmittableThreadLocal線程間傳遞邏輯示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06