開發環境
- JDK: 1.7
- spring: 4.0.6
- aspect: 1.7.4
應用背景
在APP與后台通訊的過程中,我們一般都會有個authToken的字符串校驗,判斷那些請求是需要校驗用戶信息的,因為APP用戶並不需要登錄到我們的后台系統,所以一些基於session的權限控制(比如shiro)並不合適,所以導致我們又回到了解放前,很多請求都需要先校驗這個用戶的信息,不通過的就重定向到登錄界面,比如下面的代碼:
AppUser user = appUserService.getUserByAuthToken(appointmentSearchVo.getAuthToken());
if (null == user) {
throw new BizException(ErrorMessage.CHECK_AUTHTOKEN_FAIL);
}
可以看到在很多的控制器下都會有這樣一段代碼,太丑陋了,像這些重復的但又必不可少的操作,用AOP的方式做統一處理是很優雅的,我的思路是:
- 1.定義一個查詢父類,里面包含到authToken跟usedId兩個屬性,所有需要校驗用戶的請求的查詢參數都繼承這個查詢父類,之所以會有這個userId,是因為我們校驗得到用戶之后,需要根據用戶Id獲取一些用戶數據的,所以在AOP層我們就回填了這個參數了,這樣也不會影響之前的代碼邏輯(這個可能跟我的業務需求有關了)
public class AuthSearchVO {
public String authToken; //校驗字符串
public Integer userId; //APP用戶Id
public final String getAuthToken() {
return authToken;
}
public final void setAuthToken(String authToken) {
this.authToken = authToken;
}
public final Integer getUserId() {
return userId;
}
public final void setUserId(Integer userId) {
this.userId = userId;
}
@Override
public String toString() {
return "SearchVO [authToken=" + authToken + ", userId=" + userId + "]";
}
}
- 2.定義一個方法級的注解,所有需要校驗的請求都加上這個注解,用於AOP的攔截(當然你也可以攔截所有控制器的請求)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthToken {
String type();
}
- 3.AOP處理,之所以會將注解作為參數傳進來,是因為考慮到可能會有多個APP的校驗,可以利用注解的type屬性加以區分
public class AuthTokenAOPInterceptor {
@Resource
private AppUserService appUserService;
private static final String authFieldName = "authToken";
private static final String userIdFieldName = "userId";
public void before(JoinPoint joinPoint, AuthToken authToken) throws Throwable{
Object[] args = joinPoint.getArgs(); //獲取攔截方法的參數
boolean isFound = false;
for(Object arg : args){
if(arg != null){
Class<?> clazz = arg.getClass();//利用反射獲取屬性值
Field[] fields = clazz.getDeclaredFields();
int authIndex = -1;
int userIdIndex = -1;
for(int i = 0; i < fields.length; i++){
Field field = fields[i];
field.setAccessible(true);
if(authFieldName.equals(field.getName())){//包含校驗Token
authIndex = i;
}else if(userIdFieldName.equals(field.getName())){//包含用戶Id
userIdIndex = i;
}
}
if(authIndex >= 0 & userIdIndex >= 0){
isFound = true;
authTokenCheck(fields[authIndex], fields[userIdIndex], arg, authToken);//校驗用戶
break;
}
}
}
if(!isFound){
throw new BizException(ErrorMessage.CHECK_AUTHTOKEN_FAIL);
}
}
private void authTokenCheck(Field authField, Field userIdField, Object arg, AuthToken authToken) throws Exception{
if(String.class == authField.getType()){
String authTokenStr = (String)authField.get(arg);//獲取到校驗Token
AppUser user = appUserService.getUserByAuthToken(authTokenStr);
if(user != null){
userIdField.set(arg, user.getId());
}else{
throw new BizException(ErrorMessage.CHECK_AUTHTOKEN_FAIL);
}
}
}
}
- 4.最后就是在配置文件中配置這個AOP了(因為我們的spring版本跟aspect版本有點出入,導致用不了基於注解的方式)
<bean id="authTokenAOPInterceptor" class="com.distinct.app.web.common.auth.AuthTokenAOPInterceptor"/>
<aop:config proxy-target-class="true">
<aop:pointcut id="authCheckPointcut" expression="@annotation(authToken)"/>
<aop:aspect ref="authTokenAOPInterceptor" order="1">
<aop:before method="before" pointcut-ref="authCheckPointcut"/>
</aop:aspect>
</aop:config>
最后給出測試代碼,這樣的代碼就優雅很多了
@RequestMapping(value = "/appointments", method = { RequestMethod.GET })
@ResponseBody
@AuthToken(type="disticntApp")
public List<AppointmentVo> getAppointments(AppointmentSearchVo appointmentSearchVo) {
List<AppointmentVo> appointments = appointmentService.getAppointment(appointmentSearchVo.getUserId(), appointmentSearchVo);
return appointments;
}