網站首頁 編程語言 正文
前言
Spring Security有兩種配置方式,今天重點是紹基于方法配置的方式。
基于方法配置權限
這個主要是有一些注解提供給大家使用,今天來給大家一個demo(參考自官方sample)。
maven就不多累贅了。重點看看配置。
- 基于角色配置
/**
* 啟用方法安全:@EnableMethodSecurity
*
* 啟用@secured注解: **securedEnabled = true**
* <p>會導入配置:SecuredMethodSecurityConfiguration<p>
*
* 啟用@PreAuthorize@PostAuthorize@PreFilter@PostFilter:**prePostEnabled = false**
* <p>會導入配置:PrePostMethodSecurityConfiguration</p>
*
* 啟用jsr250相關的安全注解:**jsr250Enabled = true**
* <p>會導入配置:Jsr250MethodSecurityConfiguration</p>
* <p>jsr250包括@RolesAllowed@PermitAll@DenyAll</p>
*/
@Configuration
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class AspectjSecurityConfig {
}
/**
* 這個類的所有方法都需要有ROLE_USER角色才能執行
*/
@Service
@Secured("ROLE_USER")
public class SecuredService {
public void secureMethod() {
// nothing
}
}
@Service
public class SecuredService {
public void publicMethod() {
// nothing
}
/**
* 這個方法需要ROLE_USER才能執行
* 這三種配置方式都是等價的,只是提供支持的Advice不同。
*/
@Secured("ROLE_USER")
// @RolesAllowed("ROLE_USER")
// @PreAuthorize("hasRole('ROLE_USER')")
public void secureMethod() {
// nothing
}
@PreAuthorize("arguments[0] ne 'tony'")
// @PreAuthorize("filterObject ne 'tony'")
public void preAuthorize(@RequestParam("userCode") String userCode) {
// @preAuthorize不會賦值ROOT的filterObject和returnObject,因此無法使用入參。只能使用MethodSecurityExpressionRoot的其他方法
// 這個方法的實驗現象為:不管傳什么都能通過表達式
logger.info("preAuthorize:{}", userCode);
// nothing
}
/**
* 這個入參會被過濾地只剩下與authentication.name一樣的
*/
@PreFilter("filterObject == authentication.name")
public void preFilter(@RequestParam("userCodeList") List<String> userCodeList) {
// http://localhost:8090/foo/preFilter?userCodeList=leo,tony
logger.info("preAuthorize:{}", userCode);
}
/**
* 這個方法需要ROLE_USER才能執行
*/
@PostAuthorize("returnObject ne 'tony'")
public String postAuthorize(@RequestParam("userCode") String userCode) {
// 傳入的是不是tony,會拋出異常: 403
logger.info("postAuthorize:{}", userCode);
return userCode;
}
@GetMapping("/preAuthorizeSpel")
@PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')")
public void preAuthorizeHasRole() {
logger.info("preAuthorizeSpel:{}", userCode);
// nothing
}
/**
* 這個方法需要ROLE_USER才能執行
*/
@PostFilter("returnObject == authentication.name")
public List<String> PostFilter(@RequestParam("userCodeList") List<String> userCodeList) {
// 實驗結果就是,傳入了的參數有值,但只有跟用戶名一樣的才會返回
logger.info("PostFilter:{}", userCodeList);
return userCodeList;
}
}
以上就是怎么使用,接著我們看下是如何實現的。
基于方法授權方式的實現原理
前面我們說過,是基于AOP實現的。那么,現在我們從源碼層面來看看。
我們可以看到上面按照@EnableMethodSecurity的配置,分別對應地導入三個配置。
但是,我們可以先從AOP的角度設想一下,我們需要的是哪種類型的通知?不妨歸類一下:
注解類型 | 通知類型 | 描述/備注 |
---|---|---|
@PreFilter@PreAuthorize@Secured以及JSR-250的相關注解 | 前置通知 | 這些很明顯都是需要先校驗/過濾參數再執行目標方法 |
@PostFilter@PostAuthorize | 后置通知 | 先執行目標方法,再校驗/過濾結果集 |
接著我們來看看Spring Security的設計:
AuthorizationManagerBeforeMethodInterceptor
在執行方法之前進行鑒權,這也意味著,當權限不足時,他會拋出異常。
實際上,他是一個通用的增強,全取決于你怎么使用它。
在SpringSecurity的配置里,他可以負責@PreAuthor,也可以負責@Secured,甚至還能負責jsr250的相關注解。
需要提醒一點,一個實例對象只能一種注解哈。
但是,大家有沒有想過一個問題:為什么@PreAuthor@Secured以及jsr250的相關注解,都能交給他來處理?
又或者說,他的設計是如何將這三者的處理抽象統一起來的?更具體一點,此三者有何共同之處,可以進行抽象和統一的??
要回答這個問題,我們需要先從這個三者的入手:
共同點:
都需要在執行方法前進行權限校驗,校驗不通過則都需要拋出異常,阻斷方法調用。
異同點:
注解 | 配置描述 | 執行 |
---|---|---|
@PreAuthorize | 配置的是SPEL表達式 | 通過執行表達式來得出是否滿足訪問權限 |
@Secured | 指定角色 | 需要校驗當前用戶是否擁有指定角色 |
jsr250的@PermitAll | - | 任何人都可以訪問 |
jsr250的@DenyAll | - | 任何人都不能訪問 |
jsr250的@RolesAllowed | 指定角色 | 需要校驗當前用戶是否擁有指定角色 |
從共同點出發,本質上無非就是鑒權嘛,這不是很符合Spring Security的AuthorizationManager的職責嗎?
然后就是不同點,我們發現無非就是權限的配置來源不同需要解析不同的注解咯。
有了這個思路,接下來我們翻找源碼,理解起來就容易多了。
public final class AuthorizationManagerBeforeMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
// 構造器,需要傳入Pointcut,和AuthorizationManager<MethodInvocation>,以便校驗權限。
public AuthorizationManagerBeforeMethodInterceptor(Pointcut pointcut,
AuthorizationManager<MethodInvocation> authorizationManager) {
Assert.notNull(pointcut, "pointcut cannot be null");
Assert.notNull(authorizationManager, "authorizationManager cannot be null");
this.pointcut = pointcut;
this.authorizationManager = authorizationManager;
}
/**
* 創建負責處理@PreAuthorize的增強
*/
public static AuthorizationManagerBeforeMethodInterceptor preAuthorize(
PreAuthorizeAuthorizationManager authorizationManager) {
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(PreAuthorize.class), authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE.getOrder());
return interceptor;
}
/**
* 創建負責處理@Secured
*/
public static AuthorizationManagerBeforeMethodInterceptor secured(
SecuredAuthorizationManager authorizationManager) {
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(Secured.class), authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.SECURED.getOrder());
return interceptor;
}
/**
* 創建負責處理jsr250相關注解: @RolesAllow@PermitAll@DenyAll
*/
public static AuthorizationManagerBeforeMethodInterceptor jsr250(Jsr250AuthorizationManager authorizationManager) {
AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(
AuthorizationMethodPointcuts.forAnnotations(RolesAllowed.class, DenyAll.class, PermitAll.class),
authorizationManager);
interceptor.setOrder(AuthorizationInterceptorsOrder.JSR250.getOrder());
return interceptor;
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 校驗權限
attemptAuthorization(mi);
return mi.proceed();
}
private void attemptAuthorization(MethodInvocation mi) {
// 通過AuthorizationManager校驗權限,這意味者AuthorizationManager必須具備解析相關注解的能力,實際上他交給了另外一個組件,后面會說到。
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, mi);
// 發布授權事件
this.eventPublisher.publishAuthorizationEvent(this.authentication, mi, decision);
// 不通過就拋出訪問拒絕異常
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
從源碼,我們可以發現之前分析的三類注解,只需要一個MethodInterceptor。而他們的不同之處則由AuthorizationManager這個同一個的接口進行統一調度。
對應地:
注解 | AuthorizationManager |
---|---|
@PreAuthorize | PreAuthorizeAuthorizationManager |
@Secured | SecuredAuthorizationManager |
jsr250的注解 | Jsr250AuthorizationManager |
- SecuredAuthorizationManager:
public final class SecuredAuthorizationManager implements AuthorizationManager<MethodInvocation> {
// 這是個最為簡單的AuthorizationManager實現,無非就是將當前用戶的權限與所需要的權限進行比較,如果找到就認為擁有訪問權限。
private AuthorizationManager<Collection<String>> authoritiesAuthorizationManager = new AuthoritiesAuthorizationManager();
// 緩存已經解析過的方法所對應的權限
private final Map<MethodClassKey, Set<String>> cachedAuthorities = new ConcurrentHashMap<>();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
// 獲取目標方法配置的訪問權限
Set<String> authorities = getAuthorities(mi);
// 比較權限,從authoritiesAuthorizationManager.check方法的入參也能大概猜到怎么實現
return authorities.isEmpty() ? null : this.authoritiesAuthorizationManager.check(authentication, authorities);
}
private Set<String> getAuthorities(MethodInvocation methodInvocation) {
Method method = methodInvocation.getMethod();
Object target = methodInvocation.getThis();
Class<?> targetClass = (target != null) ? target.getClass() : null;
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
// 如果尚未存在相關緩存,則進行解析。
// 由此可見,只有當目標方法被第一次執行/調用的時候才會出發解析動作
return this.cachedAuthorities.computeIfAbsent(cacheKey, (k) -> resolveAuthorities(method, targetClass));
}
private Set<String> resolveAuthorities(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// 嘗試在方法上尋找目標注解@Secured
Secured secured = findSecuredAnnotation(specificMethod);
// 返回@Secured注解所配置的權限
return (secured != null) ? Set.of(secured.value()) : Collections.emptySet();
}
}
- Jsr250AuthorizationManager
相較于SecuredAuthorizationManager,他負責的則是3個注解,而不是一個。
public final class Jsr250AuthorizationManager implements AuthorizationManager<MethodInvocation> {
// 這是AuthorizationManager的注冊器
// 因為要處理三個注解,每個注解的處理邏輯雖然簡單,但是確實不一樣。
// 而每個方法存在的注解也不一樣
private final Jsr250AuthorizationManagerRegistry registry = new Jsr250AuthorizationManagerRegistry();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation methodInvocation) {
// 根據方法,從注冊器中獲取對應的AuthorizationManager
AuthorizationManager<MethodInvocation> delegate = this.registry.getManager(methodInvocation);
// 執行授權校驗邏輯
return delegate.check(authentication, methodInvocation);
}
// 這是Jsr250AuthorizationManager的內部類
private final class Jsr250AuthorizationManagerRegistry extends AbstractAuthorizationManagerRegistry {
@NonNull
@Override
AuthorizationManager<MethodInvocation> resolveManager(Method method, Class<?> targetClass) {
// 解析注解
Annotation annotation = findJsr250Annotation(method, targetClass);
if (annotation instanceof DenyAll) {
// 返回一個lambda表達式構建的AuthorizationManager,其實現為:直接返回拒絕訪問
return (a, o) -> new AuthorizationDecision(false);
}
if (annotation instanceof PermitAll) {
// 返回一個lambda表達式構建的AuthorizationManager,其實現為:直接返回允許訪問
return (a, o) -> new AuthorizationDecision(true);
}
if (annotation instanceof RolesAllowed) {
RolesAllowed rolesAllowed = (RolesAllowed) annotation;
// 返回與@Secured一樣的AuthorityAuthorizationManager
return AuthorityAuthorizationManager.hasAnyRole(Jsr250AuthorizationManager.this.rolePrefix,
rolesAllowed.value());
}
// 這里應當看到,這三個注解的判斷順序。事實上,在解析注解的時候,只會返回其中之一。
// 而jsr250的注解也是不能同時使用的,只能用其中一個。
return NULL_MANAGER;
}
}
}
abstract class AbstractAuthorizationManagerRegistry {
private final Map<MethodClassKey, AuthorizationManager<MethodInvocation>> cachedManagers = new ConcurrentHashMap<>();
final AuthorizationManager<MethodInvocation> getManager(MethodInvocation methodInvocation) {
Method method = methodInvocation.getMethod();
Object target = methodInvocation.getThis();
Class<?> targetClass = (target != null) ? target.getClass() : null;
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
// 這里就跟@Secured類似了,只不過其需要兼顧3個注解,因此value變成了AuthorizationManager
return this.cachedManagers.computeIfAbsent(cacheKey, (k) -> resolveManager(method, targetClass));
}
abstract AuthorizationManager<MethodInvocation> resolveManager(Method method, Class<?> targetClass);
}
- PreAuthorizeAuthorizationManager
與他的同伴不同,他可是要支持SPEL的。
public final class PreAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocation> {
private PreAuthorizeExpressionAttributeRegistry registry = new PreAuthorizeExpressionAttributeRegistry();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation mi) {
// 通過方法,從注冊器獲取ExpressionAttribute。
ExpressionAttribute attribute = this.registry.getAttribute(mi);
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
// 通過注冊器找到ExpressionHandler,并創建EvaluationContext
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
// 執行SPEL表達式
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
// 返回決策:是否允許訪問
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
}
}
這里我們看到了為了支持SPEL的第一個抽象:ExpressionAttribute。他最重要的使命就是記錄表達式。
再來看看,最為重要的:PreAuthorizeExpressionAttributeRegistry
final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAttributeRegistry<ExpressionAttribute> {
private final MethodSecurityExpressionHandler expressionHandler;
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// 尋找@PreAuthorize注解
PreAuthorize preAuthorize = findPreAuthorizeAnnotation(specificMethod);
if (preAuthorize == null) {
return ExpressionAttribute.NULL_ATTRIBUTE;
}
// 解析表達式
Expression preAuthorizeExpression = this.expressionHandler.getExpressionParser()
.parseExpression(preAuthorize.value());
// 返回解析到的表達式
return new ExpressionAttribute(preAuthorizeExpression);
}
}
abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute> {
// 緩存方法對應的表達式
private final Map<MethodClassKey, T> cachedAttributes = new ConcurrentHashMap<>();
final T getAttribute(Method method, Class<?> targetClass) {
MethodClassKey cacheKey = new MethodClassKey(method, targetClass);
// 解析并緩存表達式
return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
}
}
AuthorizationManagerAfterMethodInterceptor
他與前面介紹的MethodInterceptor相呼應,一前一后,你從名字就能發現。只不過與前者相比,目前他只有一個注解需要關注@PostAuthorize。
public final class AuthorizationManagerAfterMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
private final AuthorizationManager<MethodInvocationResult> authorizationManager;
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 先執行
Object result = mi.proceed();
// 再校驗。這里應當注意到他的入參包括方法返回值
attemptAuthorization(mi, result);
return result;
}
private void attemptAuthorization(MethodInvocation mi, Object result) {
// 熟悉的配方,只不過入參變成了方法返回值罷了
MethodInvocationResult object = new MethodInvocationResult(mi, result);
AuthorizationDecision decision = this.authorizationManager.check(this.authentication, object);
this.eventPublisher.publishAuthorizationEvent(this.authentication, object, decision);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
}
public final class PostAuthorizeAuthorizationManager implements AuthorizationManager<MethodInvocationResult> {
private PostAuthorizeExpressionAttributeRegistry registry = new PostAuthorizeExpressionAttributeRegistry();
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult mi) {
ExpressionAttribute attribute = this.registry.getAttribute(mi.getMethodInvocation());
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return null;
}
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
expressionHandler.setReturnObject(mi.getResult(), ctx);
boolean granted = ExpressionUtils.evaluateAsBoolean(attribute.getExpression(), ctx);
return new ExpressionAuthorizationDecision(granted, attribute.getExpression());
}
}
經過了前面的分析,這里也就沒啥多說的了。與@PreAuthorize很相似。
PreFilterAuthorizationMethodInterceptor
負責處理@PreFilter注解的方法調用。
public final class PreFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
public PreFilterAuthorizationMethodInterceptor() {
// 默認處理的是@PreFilter
this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PreFilter.class);
}
// ...
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 從屬性注冊器中獲取到目標方法解析好的@PreFilter的相關屬性信息。
PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute attribute = this.registry.getAttribute(mi);
if (attribute == PreFilterExpressionAttributeRegistry.PreFilterExpressionAttribute.NULL_ATTRIBUTE) {
return mi.proceed();
}
// 從屬性注冊器中獲取對應的SPEL的表達式處理器,以便創建SPEL的上下文
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
// 從表達式上下文中獲取需要過濾的目標
Object filterTarget = findFilterTarget(attribute.getFilterTarget(), ctx, mi);
// 根據SPEL表達式執行過濾
expressionHandler.filter(filterTarget, attribute.getExpression(), ctx);
// 執行目標方法
return mi.proceed();
}
// ...
}
可以看出,與@PreAuthorize類似,只是少了一層AuthorizationManager的封裝。原因也很簡單,AuthorizationManager是用來鑒權的,而@PreFilter不需要鑒權。
只需要過濾參數即可。因此他也不會拋出訪問異常。
PostFilterAuthorizationMethodInterceptor
負責處理@PostFilter。
public final class PostFilterAuthorizationMethodInterceptor
implements Ordered, MethodInterceptor, PointcutAdvisor, AopInfrastructureBean {
// 這個注冊器與PreAuthorizeExpressionAttributeRegistry類似,都繼承同一個父類。
// 只有解析的注解不一樣,對應ExpressionAttribute稍稍不一樣,前者記錄有方法參數,而后者記錄有方法返回值。
private PostFilterExpressionAttributeRegistry registry = new PostFilterExpressionAttributeRegistry();
public PostFilterAuthorizationMethodInterceptor() {
this.pointcut = AuthorizationMethodPointcuts.forAnnotations(PostFilter.class);
}
@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 先執行了方法,獲得返回值
Object returnedObject = mi.proceed();
ExpressionAttribute attribute = this.registry.getAttribute(mi);
if (attribute == ExpressionAttribute.NULL_ATTRIBUTE) {
return returnedObject;
}
// 獲取ExpressionHandler,創建EvaluationContext
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(this.authentication, mi);
// 執行過濾邏輯
return expressionHandler.filter(returnedObject, attribute.getExpression(), ctx);
}
我們可以看到@PostFilter,只是對返回進行過濾,同樣也不會拋出訪問異常。
小結
分析完源碼之后,我們不難發現,每一個類都很小,且每一個方法都足夠簡單。這點是值得大家學習的。
當然,不能盲目。因為作為一款優秀的框架,必須要具備良好的可擴展性。通過分析和抽象,能夠很好起到這個作用。這是框架源碼設計者所追求的。
但是其弊端也很明顯,那就是一個完整的功能實現,往往需要諸多組件協作完成,初學者很難入門。因此,在實際業務開發過程中,不能盲目地追求這種擴展性。
同時,面對復雜業務時,不妨多分析,是否可以像框架源碼的設計者那樣思考?當然,設計完成后,需要留下相當的文檔。
總結
各方法注解的應用場景
注解 | 應用場景 | 實現原理 |
---|---|---|
@Secured | 配置需要滿足的權限,可以是角色名或者權限 | 基于AuthorityAuthorizationManager |
JSR-250的@PermitAll | 任何人都可以訪問 | 基于lambda表達式實現的簡單的AuthorizationManager |
JSR-250的@DenyAll | 任何人都不能訪問 | 基于lambda表達式實現的簡單的AuthorizationManager |
JSR-250的@RolesAllowed | 配置需要滿足的權限,可以是角色名或者權限。可以于@Secured相互取代。 | 基于AuthorityAuthorizationManager,可以于@Secured相互取代。 |
@PreAuthorize | 用于在方法調用之前鑒權,支持方法入參作為表達式的一部分 | 基于SPEL表達式 |
@PostAuthorize | 用于在方法調用之后鑒權,支持方法返回值作為表達式的一部分 | 基于SPEL表達式 |
@PreFilter | 用于過濾方法入參 | 基于SPEL表達式 |
@PostFilter | 用于過濾方法返回值 | 基于SPEL表達式 |
在搞清楚了各注解的用處和使用后,我們在回過頭來看,我們可以使用哪些授權方式:
- RBAC(基于角色的訪問控制)
這個除了@PreFilter@PostFilter,其他注解都能實現。SPEL可以使用hasAnyRole("ADMIN","USER")
后記
至此,我們應該算全方面聊完了基于方法注解怎么配置權限這個話題。不管各個注解的使用場景,亦或者其實現原理,還是各種設計思路。
下一節,我們將聊聊基于HttpRequest的權限配置方式。這個直接使用倒是簡單,但是想要定制就必須深入理解其設計和原理。加油。
參照
Method Security
Spring Security的@PreAythorize、@PostAuthorize、@PreFilter 和@PostFilter
原文鏈接:https://blog.csdn.net/Evan_L/article/details/136605487
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2022-06-24 python學習之讀取配置文件_python
- 2022-08-06 python用pd.read_csv()方法來讀取csv文件的實現_python
- 2022-07-03 C語言由淺入深了解變量的應用_C 語言
- 2022-07-03 python編碼格式導致csv讀取錯誤問題(csv.reader,?pandas.csv_read)
- 2022-05-07 Python進程間通信方式_python
- 2022-10-29 Pytorch訓練模型時如何釋放GPU顯存 torch.cuda.empty_cache()內存釋放
- 2022-12-08 python?datetime?和時間戳互相轉換問題_python
- 2022-11-26 詳解Python中的with語句和上下文管理器_python
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支