1 引言
在現代軟件開發中,尤其是在微服務架構下,服務接口的鑒權和內部認證是保障系統安全的重要環節。本文將詳細介紹PmHub中如何利用自定義注解和AOP(面向切面編程)實現服務接口的鑒權和內部認證,所涉及的技術知識點對于理解和實現系統安全具有重要意義。
2 注解的基本概念
在Java中,注解(Annotation)是一種特殊的語法,以@符號開頭,是Java 5開始引入的新特性。注解可看作特殊的注釋,用于修飾類、方法或變量,為程序在編譯或運行時提供信息。例如JDK內置的@Override注解,用于幫助編譯器檢查方法是否正確重寫父類方法。
package java.lang;import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
2.1 注解的作用
- 編譯時檢查:如
@Override
注解輔助編譯器檢查方法重寫的正確性。 - 代碼生成:像
@Entity
注解可告知框架生成對應的數據庫表。 - 運行時處理:
@Deprecated
注解在運行時提醒開發者某方法或類已不建議使用。
2.2 注解的解析方法
- 編譯期直接掃描:編譯器編譯Java代碼時掃描注解并處理,如
@Override
注解的處理。 - 運行時通過反射處理:Spring框架中的
@Value
、@Component
等注解通過反射處理,也是自定義注解常用的解析方式。
3 自定義注解的實現
自定義注解主要包括以下幾個步驟:
- 定義注解:使用
@interface
關鍵字定義注解。 - 注解元素:在注解中定義元素,就像在接口中定義方法。
- 元注解:使用元注解(如
@Retention
、@Target
等)來描述注解的行為。
3.1 定義注解
使用@interface關鍵字定義注解,例如:
import java.lang.annotation.*;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {String value() default "";int number() default 0;
}
在上面的例子中,MyAnnotation
注解有兩個元素:value
和number
。其中,number
有一個默認值0。
3.2 元注解
元注解用于描述注解本身的行為,常見元注解有:
@Retention
:指明注解的保留策略。
@Target
:指明注解的使用目標。
@Retention
:指定注解的生命周期,取值包括:RetentionPolicy.SOURCE
:注解只在源代碼中存在,編譯后消失。RetentionPolicy.CLASS
:注解在編譯后存在于.class
文件中,運行時不存在。RetentionPolicy.RUNTIME
:注解在運行時存在,可通過反射讀取。
@Target
:指定注解的使用目標,常見取值有:ElementType.TYPE
:用于類、接口、枚舉、注解類型。ElementType.FIELD
:用于字段或屬性。ElementType.METHOD
:用于方法。ElementType.PARAMETER
:用于參數。ElementType.CONSTRUCTOR
:用于構造函數。ElementType.LOCAL_VARIABLE
:用于局部變量。
3.3 自定義注解示例
下面是一個包含@Retention和@Target元注解的完整自定義注解示例:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;// 定義注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {String value();int number() default 0;
}
3.4 使用自定義注解
定義完注解后,可以在代碼中使用它:
public class Test {@MyAnnotation(value = "Test method", number = 42)public void testMethod() {// 方法的具體實現}
}
3.5 通過反射讀取注解
import java.lang.reflect.Method;public class Main {public static void main(String[] args) throws Exception {Method method = Test.class.getMethod("testMethod");if (method.isAnnotationPresent(MyAnnotation.class)) {MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);System.out.println("Value: " + annotation.value());System.out.println("Number: " + annotation.number());}}
}
4 Spring AOP簡介
AOP(面向切面編程)是一種編程范式,允許在不改變業務邏輯代碼的情況下,將橫切關注點(如日志記錄、事務管理、安全檢查等)模塊化。Spring AOP提供了多種定義和使用切面的方式:
- 注解:使用
@Aspect
和相關注解(如@Before
、@After
、@Around
等)定義切面和切點。 - XML配置:在Spring配置文件中定義切面和切點,但現代開發中較少使用。
5 微服務架構下的鑒權基礎
微服務架構由多個獨立服務組成,服務間通訊、監控等聚合而成。其優點包括提高開發效率、增強可維護性、靈活技術選型、支持持續交付和部署、實現故障隔離、按需擴展等。
鑒權是對權限認證和授權控制,專業的鑒權框架有Spring Security和Shiro等。常見鑒權方式有:
- 用戶名和密碼:傳統鑒權方式,需注意密碼存儲和傳輸安全。
- 多因素認證(MFA):通過多種驗證因素確認用戶身份,如知識因子、擁有因子、生物因子。
- OAuth(開放授權):允許第三方應用以有限權限訪問用戶資源,常用于社交登錄和API訪問控制。
- JWT(JSON Web Token):基于JSON的開放標準,用于各方之間傳遞聲明,包含用戶信息和簽名,可用于鑒權和授權,PmHub采用此方式鑒權。
6 PmHub中的鑒權認證實現
6.1 PmHub架構
PmHub采用微服務架構,有單獨的認證服務pmhub-auth。請求分為通過API網關的請求和微服務內部請求。
6.2 PmHub中的認證
PmHub 采用微服務架構,其認證流程如下:
- 登錄請求轉發
用戶發起登錄請求,該請求先到達網關(如端口 8080 )。網關根據配置的路由規則,將請求轉發到認證中心服務pmhub - auth(如端口 6800 ) 。 - 用戶信息查詢
認證服務接收到登錄請求后,依據用戶輸入的用戶名,查詢對應的用戶信息。 - 密碼校驗
從Redis中獲取密碼相關信息(文中未明確密碼存入Redis的過程,但邏輯上是從中獲取用于校驗 ),對用戶輸入的密碼進行正確性校驗。 - 記錄登錄日志
若密碼校驗通過,認證服務記錄此次登錄日志,留存登錄相關信息。 - 生成登錄token
認證服務生成JWT(JSON Web Token)字符串作為登錄token ,JWT中包含用戶信息、簽名等內容。 - 存儲token至Redis
將生成的JWT字符串存入Redis,并設置過期時間,以此維護用戶登錄狀態及確定過期時間。 - 返回token信息
認證服務將生成的token信息返回給客戶端。后續客戶端攜帶該token進行請求時,系統會先去Redis檢查JWT字符串是否存在,若存在則對其進行解析,能成功解析則認定用戶已登錄且身份合法 。
6.3 PmHub中的鑒權
6.3.1 外部請求
請求到達網關后,通過微服務的自定義請求頭攔截器(放置在公共包,各服務可引用),配合自定義注解和AOP,攔截請求頭獲取用戶和權限信息,有權限則放行,無權限則拋出異常。
6.3.2 內部請求
正常情況下內部請求無需鑒權,可以直接處理。但使用OpenFeign時,數據都是通過接口暴露出去的,為防止外部請求調用接口,采用自定義內部請求注解,AOP控制攔截,然后在內部請求調用的時候,額外加一個頭字段加以區分。
PmHub做內部請求鑒權流程如下:
- 自定義注解
定義內部認證注解InnerAuth
,使用@Target(ElementType.METHOD)
指定該注解用于方法上,@Retention(RetentionPolicy.RUNTIME)
表示注解在運行時存在,可通過反射讀取。注解包含元素isUser()
,用于標識是否校驗用戶信息,默認值為false
。代碼如下:
/*** 內部認證注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerAuth {/*** 是否校驗用戶信息*/boolean isUser() default false;
}
- AOP切面控制
創建InnerAuthAspect
類,實現Ordered
接口 ,并使用@Aspect
和@Component
注解,將其定義為一個切面并納入Spring容器管理。通過@Around("@annotation(innerAuth)")
定義環繞通知,攔截標注了InnerAuth
注解的方法。具體邏輯為:- 從請求頭中獲取
FROM_SOURCE
字段值,判斷是否等于內部請求標識SecurityConstants.INNER
,若不相等,拋出InnerAuthException
異常,提示沒有內部訪問權限。 - 若配置了需校驗用戶信息(
innerAuth.isUser()
為true
),從請求頭獲取用戶ID(DETAILS_USER_ID
)和用戶名(DETAILS_USERNAME
),若二者有空白情況,拋出InnerAuthException
異常,提示沒有設置用戶信息。 - 若上述校驗都通過,執行
point.proceed()
,放行請求,讓目標方法正常執行。
- 從請求頭中獲取
代碼如下:
/*** 內部服務調用驗證處理*/
@Aspect
@Component
public class InnerAuthAspect implements Ordered {@Around("@annotation(innerAuth)")public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable {String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);// 內部請求驗證if (!StringUtils.equals(SecurityConstants.INNER, source)) {throw new InnerAuthException("沒有內部訪問權限,不允許訪問");}String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);// 用戶信息驗證if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) {throw new InnerAuthException("沒有設置用戶信息,不允許訪問 ");}return point.proceed();}
}
- OpenFeign請求攔截器處理
實現feign.RequestInterceptor
接口,創建FeignRequestInterceptor
類,并使用@Component
注解納入Spring容器管理 。在apply(RequestTemplate requestTemplate)
方法中:- 獲取當前請求
HttpServletRequest
。 - 從請求頭中提取用戶ID(
DETAILS_USER_ID
)、用戶密鑰(USER_KEY
)、用戶名(DETAILS_USERNAME
)、認證信息(AUTHORIZATION_HEADER
)等信息,若存在則設置到RequestTemplate
的請求頭中,防止用戶信息在OpenFeign調用時丟失。 - 獲取客戶端IP,通過
X-Forwarded-For
頭字段設置到請求頭中。
- 獲取當前請求
代碼如下:
/*** feign請求攔截器*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {@Overridepublic void apply(RequestTemplate requestTemplate) {HttpServletRequest httpServletRequest = ServletUtils.getRequest();if (StringUtils.isNotNull(httpServletRequest)) {Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);// 傳遞用戶信息請求頭,防止丟失String userId = headers.get(SecurityConstants.DETAILS_USER_ID);if (StringUtils.isNotEmpty(userId)) {requestTemplate.header(SecurityConstants.DETAILS_USER_ID, userId);}String userKey = headers.get(SecurityConstants.USER_KEY);if (StringUtils.isNotEmpty(userKey)) {requestTemplate.header(SecurityConstants.USER_KEY, userKey);}String userName = headers.get(SecurityConstants.DETAILS_USERNAME);if (StringUtils.isNotEmpty(userName)) {requestTemplate.header(SecurityConstants.DETAILS_USERNAME, userName);}String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);if (StringUtils.isNotEmpty(authentication)) {requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);}// 配置客戶端IPrequestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr());}}
}
7 總結
本文圍繞PmHub展開,介紹了Java注解的概念、實現,Spring AOP的方式。闡述微服務架構下鑒權基礎,詳細說明PmHub的鑒權認證流程,包括認證、外部及內部請求鑒權,旨在助力開發者理解和實現系統安全功能。
8 參考鏈接
- PmHub自定義注解加 AOP 實現服務接口鑒權和內部認證
- 項目倉庫(GitHub)
- 項目倉庫(碼云):(國內訪問速度更快)