SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法
最近在做項(xiàng)目的過(guò)程中 需要用JWT做登錄和鑒權(quán) 查了很多資料 都不甚詳細(xì)
有的是需要在application.yml里進(jìn)行jwt的配置 但我在導(dǎo)包后并沒(méi)有相應(yīng)的配置項(xiàng) 因而并不適用
在踩過(guò)很多坑之后 稍微整理了一下 做個(gè)筆記
一、概念
1、什么是JWT
Json Web Token (JWT)是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519)
該token被設(shè)計(jì)為緊湊且安全的 特別適用于分布式站點(diǎn)的單點(diǎn)登錄(SSO)場(chǎng)景
隨著JWT的出現(xiàn) 使得校驗(yàn)方式更加簡(jiǎn)單便捷化
JWT實(shí)際上就是一個(gè)字符串 它由三部分組成:頭部 載荷和簽名
用[.]分隔這三個(gè)部分 最終的格式類似于:xxxx.xxxx.xxxx
在服務(wù)器直接根據(jù)token取出保存的用戶信息 即可對(duì)token的可用性進(jìn)行校驗(yàn) 使得單點(diǎn)登錄更為簡(jiǎn)單
2、JWT校驗(yàn)的過(guò)程
1、瀏覽器發(fā)送用戶名和密碼 發(fā)起登錄請(qǐng)求
2、服務(wù)端驗(yàn)證身份 根據(jù)算法將用戶標(biāo)識(shí)符打包生成token字符串 并且返回給瀏覽器
3、當(dāng)瀏覽器需要發(fā)起請(qǐng)求時(shí) 將token一起發(fā)送給服務(wù)器
4、服務(wù)器發(fā)現(xiàn)數(shù)據(jù)中攜帶有token 隨即進(jìn)行解密和鑒權(quán)
5、校驗(yàn)成功 服務(wù)器返回請(qǐng)求的數(shù)據(jù)
二、使用
1、首先是導(dǎo)包
<!-- Spring Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Spring Security和JWT整合 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- 字符串轉(zhuǎn)換需要用到此包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.4</version> </dependency>
2、實(shí)體類
兩個(gè)實(shí)體類 一個(gè)是用戶 另一個(gè)是權(quán)限
public class User {
private Integer id;
private String username;
private String password;
省略gettersetter之類的代碼...
}
public class Role {
private Integer id;
private String username;
private String name;
省略gettersetter之類的代碼...
}
3、然后需要一個(gè)Utils工具類
該類用于進(jìn)行Token的加密和解密 可在此類中單元測(cè)試
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtTokenUtil {
// Token請(qǐng)求頭
public static final String TOKEN_HEADER = "Authorization";
// Token前綴
public static final String TOKEN_PREFIX = "Bearer ";
// 簽名主題
public static final String SUBJECT = "piconjo";
// 過(guò)期時(shí)間
public static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7;
// 應(yīng)用密鑰
public static final String APPSECRET_KEY = "piconjo_secret";
// 角色權(quán)限聲明
private static final String ROLE_CLAIMS = "role";
/**
* 生成Token
*/
public static String createToken(String username,String role) {
Map<String,Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
String token = Jwts
.builder()
.setSubject(username)
.setClaims(map)
.claim("username",username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
.signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
return token;
}
/**
* 校驗(yàn)Token
*/
public static Claims checkJWT(String token) {
try {
final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 從Token中獲取username
*/
public static String getUsername(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("username").toString();
}
/**
* 從Token中獲取用戶角色
*/
public static String getUserRole(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.get("role").toString();
}
/**
* 校驗(yàn)Token是否過(guò)期
*/
public static boolean isExpiration(String token){
Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
return claims.getExpiration().before(new Date());
}
}
4、配置UserDetailsService的實(shí)現(xiàn)類 用于加載用戶信息
import xxx.xxx.xxx.bean.Role; // 自己的包
import xxx.xxx.xxx.bean.User; // 自己的包
import xxx.xxx.xxx.mapper.UserMapper; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if (s == null || "".equals(s))
{
throw new RuntimeException("用戶不能為空");
}
// 調(diào)用方法查詢用戶
User user = userMapper.findUserByUsername(s);
if (user == null)
{
throw new RuntimeException("用戶不存在");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role:userMapper.findRoleByUsername(s))
{
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getName()));
}
return new org.springframework.security.core.userdetails.User(user.getUsername(),"{noop}"+user.getPassword(),authorities);
}
}
5、然后 配置兩個(gè)攔截器
其中 一個(gè)用于登錄 另一個(gè)用于鑒權(quán)
JWTAuthenticationFilter登錄攔截器:
該攔截器用于獲取用戶登錄的信息
至于具體的驗(yàn)證 只需創(chuàng)建一個(gè)token并調(diào)用authenticationManager的authenticate()方法
讓Spring security驗(yàn)證即可 驗(yàn)證的事交給框架
import com.alibaba.fastjson.JSON;
import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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.Collection;
/**
* 驗(yàn)證用戶名密碼正確后 生成一個(gè)token并將token返回給客戶端
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
/**
* 驗(yàn)證操作 接收并解析用戶憑證
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {
// 從輸入流中獲取到登錄的信息
// 創(chuàng)建一個(gè)token并調(diào)用authenticationManager.authenticate() 讓Spring security進(jìn)行驗(yàn)證
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(request.getParameter("username"),request.getParameter("password")));
}
/**
* 驗(yàn)證【成功】后調(diào)用的方法
* 若驗(yàn)證成功 生成token并返回
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,HttpServletResponse response,FilterChain chain,Authentication authResult) throws IOException {
User user= (User) authResult.getPrincipal();
// 從User中獲取權(quán)限信息
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
// 創(chuàng)建Token
String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString());
// 設(shè)置編碼 防止亂碼問(wèn)題
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// 在請(qǐng)求頭里返回創(chuàng)建成功的token
// 設(shè)置請(qǐng)求頭為帶有"Bearer "前綴的token字符串
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
// 處理編碼方式 防止中文亂碼
response.setContentType("text/json;charset=utf-8");
// 將反饋塞到HttpServletResponse中返回給前臺(tái)
response.getWriter().write(JSON.toJSONString("登錄成功"));
}
/**
* 驗(yàn)證【失敗】調(diào)用的方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
String returnData="";
// 賬號(hào)過(guò)期
if (failed instanceof AccountExpiredException) {
returnData="賬號(hào)過(guò)期";
}
// 密碼錯(cuò)誤
else if (failed instanceof BadCredentialsException) {
returnData="密碼錯(cuò)誤";
}
// 密碼過(guò)期
else if (failed instanceof CredentialsExpiredException) {
returnData="密碼過(guò)期";
}
// 賬號(hào)不可用
else if (failed instanceof DisabledException) {
returnData="賬號(hào)不可用";
}
//賬號(hào)鎖定
else if (failed instanceof LockedException) {
returnData="賬號(hào)鎖定";
}
// 用戶不存在
else if (failed instanceof InternalAuthenticationServiceException) {
returnData="用戶不存在";
}
// 其他錯(cuò)誤
else{
returnData="未知異常";
}
// 處理編碼方式 防止中文亂碼
response.setContentType("text/json;charset=utf-8");
// 將反饋塞到HttpServletResponse中返回給前臺(tái)
response.getWriter().write(JSON.toJSONString(returnData));
}
}
JWTAuthorizationFilter權(quán)限校驗(yàn)攔截器:
當(dāng)訪問(wèn)需要權(quán)限校驗(yàn)的URL(當(dāng)然 該URL也是需要經(jīng)過(guò)配置的) 則會(huì)來(lái)到此攔截器 在該攔截器中對(duì)傳來(lái)的Token進(jìn)行校驗(yàn)
只需告訴Spring security該用戶是否已登錄 并且是什么角色 擁有什么權(quán)限即可
import xxx.xxx.xxx.utils.JwtTokenUtil; // 自己的包
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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.ArrayList;
import java.util.Collection;
/**
* 登錄成功后 走此類進(jìn)行鑒權(quán)操作
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* 在過(guò)濾之前和之后執(zhí)行的事件
*/
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
// 若請(qǐng)求頭中沒(méi)有Authorization信息 或是Authorization不以Bearer開頭 則直接放行
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX))
{
chain.doFilter(request, response);
return;
}
// 若請(qǐng)求頭中有token 則調(diào)用下面的方法進(jìn)行解析 并設(shè)置認(rèn)證信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
/**
* 從token中獲取用戶信息并新建一個(gè)token
*
* @param tokenHeader 字符串形式的Token請(qǐng)求頭
* @return 帶用戶名和密碼以及權(quán)限的Authentication
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
// 去掉前綴 獲取Token字符串
String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
// 從Token中解密獲取用戶名
String username = JwtTokenUtil.getUsername(token);
// 從Token中解密獲取用戶角色
String role = JwtTokenUtil.getUserRole(token);
// 將[ROLE_XXX,ROLE_YYY]格式的角色字符串轉(zhuǎn)換為數(shù)組
String[] roles = StringUtils.strip(role, "[]").split(", ");
Collection<SimpleGrantedAuthority> authorities=new ArrayList<>();
for (String s:roles)
{
authorities.add(new SimpleGrantedAuthority(s));
}
if (username != null)
{
return new UsernamePasswordAuthenticationToken(username, null,authorities);
}
return null;
}
}
6、再配置一個(gè)自定義類 用于進(jìn)行匿名用戶訪問(wèn)資源時(shí)無(wú)權(quán)限的處理
該類需實(shí)現(xiàn)AuthenticationEntryPoint
import com.alibaba.fastjson.JSONObject;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("text/javascript;charset=utf-8");
response.getWriter().print(JSONObject.toJSONString("您未登錄,沒(méi)有訪問(wèn)權(quán)限"));
}
}
7、最后 將這些組件組裝到一起即可
創(chuàng)建一個(gè)自定義的配置類 繼承WebSecurityConfigurerAdapter
在該類上 需加@EnableWebSecurity注解 配置Web安全過(guò)濾器和啟用全局認(rèn)證機(jī)制
import xxx.xxx.xxx.JWTAuthenticationEntryPoint; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthenticationFilter; // 自己的包
import xxx.xxx.xxx.xxx.JWTAuthorizationFilter; // 自己的包
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
/**
* 安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 跨域共享
http.cors()
.and()
// 跨域偽造請(qǐng)求限制無(wú)效
.csrf().disable()
.authorizeRequests()
// 訪問(wèn)/data需要ADMIN角色
.antMatchers("/data").hasRole("ADMIN")
// 其余資源任何人都可訪問(wèn)
.anyRequest().permitAll()
.and()
// 添加JWT登錄攔截器
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
// 添加JWT鑒權(quán)攔截器
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement()
// 設(shè)置Session的創(chuàng)建策略為:Spring Security永不創(chuàng)建HttpSession 不使用HttpSession來(lái)獲取SecurityContext
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 異常處理
.exceptionHandling()
// 匿名用戶訪問(wèn)無(wú)權(quán)限資源時(shí)的異常
.authenticationEntryPoint(new JWTAuthenticationEntryPoint());
}
/**
* 跨域配置
* @return 基于URL的跨域配置信息
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 注冊(cè)跨域配置
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
定義一個(gè)用于測(cè)試的對(duì)外映射接口:
@RestController
public class UserController {
@GetMapping("/data")
private ResponseUtil data()
{
return "This is data.";
}
}
默認(rèn)登錄路徑是/login 用POST請(qǐng)求發(fā)送
若要修改默認(rèn)的登錄路徑 只需要在自己定義的登錄過(guò)濾器JWTAuthenticationFilter的構(gòu)造方法里進(jìn)行配置即可
比如 若想修改為/api/login:
public JWTAuthenticationFilter(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
// 設(shè)置登錄URL
super.setFilterProcessesUrl("/api/login");
}
登錄時(shí) 參數(shù)的屬性名分別是username和password 不能改動(dòng):

登錄成功后會(huì)返回一個(gè)Token:

在請(qǐng)求需要權(quán)限的接口路徑時(shí) 若不帶上Token 則會(huì)提示沒(méi)有訪問(wèn)權(quán)限

帶上Token后再次請(qǐng)求 即可正常訪問(wèn):
注:Token的前面要帶有Bearer 的前綴

這樣 一個(gè)基本的實(shí)現(xiàn)就差不多完成了
為簡(jiǎn)單演示 在該案例中就不對(duì)密碼進(jìn)行加密了 實(shí)際開發(fā)是需要對(duì)明文密碼加密后存儲(chǔ)的 推薦用BCrypt進(jìn)行加密和解密
為節(jié)省篇幅 用于注冊(cè)的接口也不寫了 實(shí)際上在注冊(cè)接口傳入的密碼也需要用BCrypt加密后再存入數(shù)據(jù)庫(kù)中
還可以用Redis進(jìn)行Token的存儲(chǔ) 這些都是后話了
到此這篇關(guān)于SpringBoot集成Spring Security用JWT令牌實(shí)現(xiàn)登錄和鑒權(quán)的方法的文章就介紹到這了,更多相關(guān)SpringBoot JWT令牌登錄和鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot整合SpringSecurity和JWT的示例
- SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法
- Springboot集成Spring Security實(shí)現(xiàn)JWT認(rèn)證的步驟詳解
- SpringBoot3.0+SpringSecurity6.0+JWT的實(shí)現(xiàn)
- 詳解SpringBoot+SpringSecurity+jwt整合及初體驗(yàn)
- SpringBoot+SpringSecurity+JWT實(shí)現(xiàn)系統(tǒng)認(rèn)證與授權(quán)示例
- SpringBoot集成Spring security JWT實(shí)現(xiàn)接口權(quán)限認(rèn)證
- SpringBoot3.x接入Security6.x實(shí)現(xiàn)JWT認(rèn)證的完整步驟
- springboot+springsecurity+mybatis+JWT+Redis?實(shí)現(xiàn)前后端離實(shí)戰(zhàn)教程
- SpringBoot3集成SpringSecurity+JWT的實(shí)現(xiàn)
相關(guān)文章
Maven中optional和scope元素的使用弄明白了嗎
這篇文章主要介紹了Maven中optional和scope元素的使用弄明白了嗎,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析
這篇文章主要介紹了SpringBoot中的@ControllerAdvice使用方法詳細(xì)解析, 加了@ControllerAdvice的類為那些聲明了@ExceptionHandler、@InitBinder或@ModelAttribute注解修飾的 方法的類而提供的專業(yè)化的@Component,以供多個(gè) Controller類所共享,需要的朋友可以參考下2024-01-01
SpringBoot常見(jiàn)get/post請(qǐng)求參數(shù)處理、參數(shù)注解校驗(yàn)及參數(shù)自定義注解校驗(yàn)詳解
這篇文章主要給大家介紹了關(guān)于SpringBoot常見(jiàn)get/post請(qǐng)求參數(shù)處理、參數(shù)注解校驗(yàn)及參數(shù)自定義注解校驗(yàn)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03
java中JSONArray互相轉(zhuǎn)換List的實(shí)現(xiàn)
本文主要介紹了java中JSONArray互相轉(zhuǎn)換List的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
Java web數(shù)據(jù)可視化實(shí)現(xiàn)原理解析
這篇文章主要介紹了Java web數(shù)據(jù)可視化實(shí)現(xiàn)原理解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03
啟動(dòng)Tomcat時(shí)出現(xiàn)大量亂碼的解決方法
今天給大家?guī)?lái)的是關(guān)于Java的相關(guān)知識(shí),文章圍繞著啟動(dòng)Tomcat時(shí)出現(xiàn)大量亂碼的解決方法展開,文中有非常詳細(xì)的介紹及圖文示例,需要的朋友可以參考下2021-06-06
SpringBoot+Vue前后端分離實(shí)現(xiàn)請(qǐng)求api跨域問(wèn)題
這篇文章主要介紹了SpringBoot+Vue前后端分離實(shí)現(xiàn)請(qǐng)求api跨域問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
詳解Java線程池隊(duì)列中的延遲隊(duì)列DelayQueue
這篇文章主要為大家詳細(xì)介紹了Java線程池隊(duì)列中的延遲隊(duì)列DelayQueue的相關(guān)資料,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-12-12

