SpringSecurity進(jìn)行認(rèn)證與授權(quán)的示例代碼
一、SpringSecurity簡介
Spring Security 是 Spring 家族中的一個安全管理框架。相比與另外一個安全框架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)具體是哪個用戶
- 授權(quán):經(jīng)過認(rèn)證后判斷當(dāng)前用戶是否有權(quán)限進(jìn)行某個操作
而認(rèn)證和授權(quán)也是SpringSecurity作為安全框架的核心功能。
1.1 入門Demo
依賴如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>引入依賴后我們在嘗試去訪問之前的接口就會自動跳轉(zhuǎn)到一個SpringSecurity的默認(rèn)登陸頁面,默認(rèn)用戶名是user,密碼會輸出在控制臺。

必須登陸之后才能對接口進(jìn)行訪問。

訪問 localhost:8080/logout 這個鏈接可以 對其進(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í)就是一個過濾器鏈,內(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)證流程詳解
箭頭代表該方法屬于這個實(shí)現(xiàn)類的。

概念速查:
- Authentication接口: 它的實(shí)現(xiàn)類,表示當(dāng)前訪問系統(tǒng)的用戶,封裝了用戶相關(guān)信息。
- AuthenticationManager接口:定義了認(rèn)證Authentication的方法
- UserDetailsService接口:加載用戶特定數(shù)據(jù)的核心接口。里面定義了一個根據(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
在這個實(shí)現(xiàn)類中去查詢數(shù)據(jù)庫
校驗(yàn)
①定義Jwt 認(rèn)證過濾器
獲取token 解析token獲取其中的userid
從redis中獲取用戶信息
存入SecurityContextHolder

這里為什么要存入 SecurityContextHolder中呢?
我們自定義的JWT過濾器的時候,肯定是需要將這個JWT過濾器放在UsernamePasswordAuthenticationFilter前的,這時我們將從redis獲取的用戶信息存入SecurityContextHolder才行,否則后續(xù)過濾器在進(jìn)行校驗(yàn)的時候,可能會因?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)建時間
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新時間
*/
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("用戶名或者密碼錯誤");
}
//TODO 查詢對應(yīng)的權(quán)限信息
return new LoginUser(user);
}
}
因?yàn)閁serDetailsService方法的返回值是UserDetails(接口):

所以需要定義一個類,實(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}。例如:

這樣登陸的時候就可以用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)。
我們可以定義一個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對這個接口放行,讓用戶訪問這個接口的時候不用登錄也能訪問。(畢竟登錄接口如果還需要權(quán)限訪問,那么就很奇怪了)
在接口中我們通過AuthenticationManager的authenticate方法來進(jìn)行用戶認(rèn)證,所以需要在SecurityConfig中配置把AuthenticationManager注入容器中。
認(rèn)證成功的話要生成一個JWT,放入響應(yīng)中返回,并且為了讓用戶下回請求時需要通過JWT識別出具體的是哪個用戶,我們需要把用戶信息存入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生成一個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又是一個接口,所以需要傳入其實(shí)現(xiàn)類。
2.3.4 認(rèn)證過濾器
我們需要自定義一個過濾器,這個過濾器會去獲取請求頭中的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)入下一個攔截器,后續(xù)Security框架還有很多攔截器可以對其進(jìn)行驗(yàn)證,而使用return是因?yàn)楹罄m(xù)在進(jìn)行參數(shù)返回的時候,不需要再執(zhí)行以下代碼。
簡單來說:圖中代碼紅線以上部分是參數(shù)請求時候所執(zhí)行的部分,紅線以下是返回響應(yīng)體時候所執(zhí)行的部分。

還有個細(xì)節(jié)是:這里必須調(diào)用三個參數(shù)的構(gòu)造方法,而不是兩個,這是因?yàn)橹挥姓{(diào)用三個構(gòu)造方法的時候,才能保證該主體是認(rèn)證過的,否則框架檢查時候還是會報錯:

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 退出登錄
我們只需要定義一個登陸接口,然后獲取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)證的時候,需要先在SecurityContextHolder中拿到信息后,再從redis中獲取對應(yīng)信息。
每個請求都對應(yīng)一個SecurityContextHolder,所以刪除SecurityContextHolder中的信息是無效的,需要刪除redis中所存儲的信息。
三、授權(quán)
3.1 權(quán)限系統(tǒng)作用
例如一個學(xué)校圖書館的管理系統(tǒng),如果是普通學(xué)生登錄就能看到借書還書相關(guān)的功能,不可能讓他看到并且去使用添加書籍信息,刪除書籍信息等功能。但是如果是一個圖書館管理員的賬號登錄了,應(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ù)庫中查詢的)。

而最終這個loginUser對象會返回給這個authenticate對象,那么authenticate這個對象就會存入redis中。

以上就是整個流程,所以我們最終只需要完成兩個步驟即可:
- 在查詢數(shù)據(jù)庫的時候獲取對應(yīng)的權(quán)限信息。
- 在實(shí)現(xiàn)認(rèn)證過濾器的時候,需要獲取當(dāng)前用戶的權(quán)限信息,并存入到SecurityContextHolder中。
也就是需要完善上述圖片中兩個 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)識這是個字符串,實(shí)際上我們通過idea的提示(ctrl+p),也可以知道,這個hasAuthority所需要的參數(shù)也是String類型:

3.3.2 封裝權(quán)限信息
我們前面在寫UserDetailsServiceImpl的時候說過,在查詢出用戶后還要獲取對應(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這個屬性存儲的是權(quán)限信息。
在getAuthorities方法中,由于返回值是一個Collection類型,所以我們這里選擇List集合。
又因?yàn)榉盒鸵笫荊rantedAuthority的子類,但是其是一個接口,所以我們通過查找其實(shí)現(xiàn)類(ctrl+alt+鼠標(biāo)左鍵):

選擇了SimpleGrantedAuthority,因?yàn)榭此钟袀€simple,再觀察其構(gòu)造方法,發(fā)現(xiàn)只需要傳入一個字符串即可,于是我們就需要將我們類屬性的permission全部都通過構(gòu)造方法存入SimpleGrantedAuthority屬性中,然后將其一個個遍歷,放入list集合中。

還有一個小細(xì)節(jié)是這里加了@JSONField(serialize = false) 注解防止redis存儲loginUser時候序列化出錯(報異常),因?yàn)镾impleGrantedAuthority是Spring中提供的類。
雖然我們這里不讓這個List集合序列化,但是并不影響后續(xù)操作,因?yàn)樵谌〕鰜矸葱蛄谢臅r候,我們自定義的permission屬性是可以被正常序列化的,那個時候通過它就可以讓程序正常運(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)限信息即可。
所以我們可以先定義個mapper,其中提供一個方法可以根據(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("用戶名或者密碼錯誤");
}
// 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)這個功能我們需要知道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)建一個自定義的實(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請求時必須遵守同源策略,否則就是跨域的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("*")
// 跨域允許時間
.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方法可以傳入多個權(quán)限,只有用戶有其中任意一個權(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_ 這個前綴才可以。
@RequestMapping("/hello")
@PreAuthorize("hasRole('system:dept:list')")
public String hello(){
return "hello";
}hasAnyRole 有任意的角色就可以訪問。它內(nèi)部也會把我們傳入的參數(shù)拼接上 ROLE_ 后再去比較。所以這種情況下要用用戶對應(yīng)的權(quán)限也要有 ROLE_ 這個前綴才可以。
@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)用這個對象的
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í)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解
這篇文章主要介紹了SpringCloud中的路由網(wǎng)關(guān)鑒權(quán)熔斷詳解,Hystrix是一個用于處理分布式系統(tǒng)的延遲和容錯的開源庫,在分布式系統(tǒng)里,許多依賴不可避免的會調(diào)用失敗,比如超時、異常等,需要的朋友可以參考下2024-01-01
elasticsearch如何根據(jù)條件刪除數(shù)據(jù)
Elasticsearch是一個基于Apache Lucene?的開源搜索引擎,無論在開源還是專有領(lǐng)域,Lucene 可以被認(rèn)為是迄今為止最先進(jìn)、性能最好的、功能最全的搜索引擎庫,這篇文章主要介紹了elasticsearch如何根據(jù)條件刪除數(shù)據(jù),需要的朋友可以參考下2023-03-03
Java的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-12
TransmittableThreadLocal線程間傳遞邏輯示例解析
這篇文章主要介紹了TransmittableThreadLocal線程間傳遞邏輯示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06

