基於Spring Boot AOP使用者許可權系統模組開發
公司專案需要涉及到使用者許可權的問題,每個使用者都應該有自己的許可權,而且許可權應該是靈活可變的,系統的登陸模組因為涉及到分散式部署的問題以及前後端分離,不能採用傳統的session作為登陸方式,而是採用JWT的方式實現,保證了介面的無狀態性,但是這樣的話也就讓市面上的很多許可權控制和登陸框架顯得有些不太適合,比如:Spring Security、Apache Shiro,也許能將這些框架強行塞進系統裡面,但是卻可能不適應目前的這個系統需求,靈活性不夠,也不好完美的適應現有的登入模組,忘了當時是怎麼考慮的,仔細評估和分析之後還是放棄了現有的一些框架而選擇自己重新開發這個許可權模組。
首先,我們看一下使用者與模組、模組與介面的關係:
在這張圖裡面,使用者與模組,模組與介面的關係可以這麼來說:
一個使用者可以同時擁有多個模組的許可權,一個模組也可以同時被多個使用者所擁有,而一個模組下面可以有多個介面,一個介面又可以屬於多個模組。
這樣的話,就必須保證程式碼的鬆耦合,同時又要能完美的控制使用者許可權,保證系統的安全和許可權職責的清晰。
首先,利用Spring Tool Suite建立一個Spring Boot的Web工程,引入Mybatis、Druid、Spring AOP等工具。
為了不影響現有功能,同時也必須要方便為介面新增許可權控制,這裡我們就需要涉及到面向切面的技術了,Spring的兩大特徵之一。這樣的話,我們就可以保證許可權模組與主程式之間的耦合降到最低,提供系統的可維護性和可操作性。
我們新建一個註解類PermissionModule,用來為介面添加註解,標識介面的所屬模組,程式碼如下:
package org.opensource.pri.annotations; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import java.lang.annotation.Retention; import java.lang.annotation.Target; import org.opensource.pri.enums.Module; /** * * 對方法實現模組許可權控制 * * @author TanRq * @date 2017年11月14日 * * @param belong * */ @Retention(RUNTIME) @Target(METHOD) public @interface PermissionModule { /** * 執行方法所需要的許可權,ALL表示所有許可權都可以執行 * belong的型別為陣列,方便為介面新增多個模組,即一個介面可以屬於多個模組 * 同時也可以反映出一個模組可以包含多個介面的關係 * @return */ public Module[] belong() default {Module.ALL}; }
剛剛我們說了,我們需要利用到面向切面的技術,那麼,在這裡我們新建一個類專門用來配置切面,程式碼如下:
package org.opensource.pri.config;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.opensource.pri.annotations.PermissionModule;
import org.opensource.pri.auth.AuthContext;
import org.opensource.pri.auth.AuthStrategy;
import org.opensource.pri.enums.Module;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 許可權認證配置,利用切面技術
* @author TanRq
*
*/
@Aspect
@Component
public class AuthConfig {
@Autowired
private AuthStrategy authStrategy;
@Around("execution(* org.opensource.pri.controller..*(..)) and @annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object executeAround(ProceedingJoinPoint jp) throws Throwable{
//獲取RequestAttributes
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//從獲取RequestAttributes中獲取HttpServletRequest的資訊
HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
HttpServletResponse response=((ServletRequestAttributes)requestAttributes).getResponse();
String userId= request.getHeader("userId");//獲取請求頭裡面使用者的ID,此處只做簡單處理,實際應該有加密,否則將會導致使用者資訊洩露,無法保證安全
Signature signature = jp.getSignature();
MethodSignature methodSignature = (MethodSignature)signature;
Method targetMethod = methodSignature.getMethod();
Method realMethod = jp.getTarget().getClass().getDeclaredMethod(signature.getName(), targetMethod.getParameterTypes());
Object obj =null;
if(isHasPermission(realMethod,userId)) {
obj = jp.proceed();//使用者擁有該方法許可權時執行方法裡面的內容
}else {//使用者沒有許可權,則直接返回沒有許可權的通知
response.setHeader("Content-type","application/json; charset=UTF-8");
OutputStream outputStream=response.getOutputStream();
Map<String,String> resultMsg=new HashMap<String,String>();
resultMsg.put("msg", "Not allowed to pass, you do not have the authority");
outputStream.write(new ObjectMapper().writeValueAsString(resultMsg).getBytes("UTF-8"));
}
return obj;
}
/**
* 判斷使用者是否擁有許可權
* @param realMethod
* @param userId
* @return
*/
private boolean isHasPermission(Method realMethod,String userId) {
try {
if(realMethod.isAnnotationPresent(PermissionModule.class)) {
PermissionModule permissionModule=realMethod.getAnnotation(PermissionModule.class);
Module[] modules= permissionModule.belong();
//執行許可權策略,判斷使用者許可權
return new AuthContext(authStrategy).execute(modules,userId);
}
}catch(Exception e) {
System.out.println(e.getMessage());
return false;
}
return false;
}
}
上面的切面配置類中涉及到了許可權策略,此處採用了策略模式進行程式碼的解耦,主要考慮到使用者型別可能較多,控制權限的方式可能存在多樣性,方便後期的維護,因為只需要切換策略就可以達到不同使用者的策略處理方式。我們看一下涉及到許可權策略的三個類是如何編寫:
package org.opensource.pri.auth;
import org.opensource.pri.enums.Module;
/**
* 許可權策略上下文控制類
* @author TanRq
*
*/
public class AuthContext {
private AuthStrategy authStrategy;
public AuthContext(AuthStrategy strategy) {
this.authStrategy=strategy;
}
/**
* 執行策略
* @param modules
* @param userId
* @return
* @throws Exception
*/
public boolean execute(Module[] modules,String userId) throws Exception {
return this.authStrategy.executeAuth(modules,userId);
}
}
package org.opensource.pri.auth;
import org.opensource.pri.enums.Module;
/**
* 策略介面,所有策略都應該實現的介面
* @author TanRq
*
*/
public interface AuthStrategy {
public boolean executeAuth(Module[] modules,String userId) throws Exception;
}
package org.opensource.pri.auth.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.opensource.pri.auth.AuthStrategy;
import org.opensource.pri.enums.Module;
import org.opensource.pri.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
*
* 使用者許可權控制策略演算法實現類
* @author TanRq
*
*/
@Component
public class UserAuthStrategy implements AuthStrategy {
@Autowired
private AuthService authService;
@Override
public boolean executeAuth(Module[] modules,String userId) throws Exception {
//表示標註的方式屬於所有使用者可執行
if(ArrayUtils.contains(modules, Module.ALL)) {
return true;
}
//使用者ID為空,不允許通過,直接返回false
if(StringUtils.isBlank(userId)) {
return false;
}
List<Map<String,String>> permissionList = authService.getPermission(Integer.parseInt(userId));
List<String> moduleList=new ArrayList<String>();
for(Module module:modules) {
moduleList.add(module.getModuleName());
}
List<String> hasList=new ArrayList<String>();
for(Map<String,String> map:permissionList) {
hasList.add(map.get("priName").toString());
}
//如果使用者擁有該介面所屬模組的任何一個模組的許可權則返回true,否則false
return hasList.removeAll(moduleList);
}
}
通過上面的方式,我們基本就可以完成一個許可權模組程式碼的編寫,至於其他一些非主要的類將會提供下載連線方便下載demo並檢視。
接下來就是驗證功能的時候了,我們新建一個控制器類,然後通過postman來發送請求,測試許可權功能是否能正常使用,控制器類如下:
package org.opensource.pri.controller;
import java.util.Map;
import org.opensource.pri.annotations.PermissionModule;
import org.opensource.pri.enums.Module;
import org.opensource.pri.service.HelloWorldService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 一個簡單的rest介面控制器
* @author TanRq
*
*/
@RestController
public class HelloWorldController {
@Autowired
HelloWorldService helloWorldService;
/**
* Module.ALL型別表示所有使用者都能訪問該介面
* @return
*/
@PermissionModule(belong= {Module.ALL})
@RequestMapping(value = "/sayhello",produces = "application/json; charset=UTF-8", method = RequestMethod.GET)
public Map<String,Object> sayHello(){
return helloWorldService.sayHello();
}
/**
* Module.TOTAL_EVERY_NO表示該介面只屬於TOTAL_EVERY_NO所代表的模組,
* 如果使用者沒有TOTAL_EVERY_NO的許可權,將無法訪問該介面的內容
* @return
*/
@PermissionModule(belong= {Module.TOTAL_EVERY_NO})
@RequestMapping(value = "/everyhello",produces = "application/json; charset=UTF-8", method = RequestMethod.GET)
public Map<String,Object> sayEveryHello(){
return helloWorldService.sayHello();
}
/**
* Module.TOTAL_PRE_NO表示該介面只屬於TOTAL_PRE_NO所代表的模組,
* 如果使用者沒有TOTAL_PRE_NO的許可權,將無法訪問該介面的內容
* @return
*/
@PermissionModule(belong= {Module.TOTAL_PRE_NO})
@RequestMapping(value = "/prehello",produces = "application/json; charset=UTF-8", method = RequestMethod.GET)
public Map<String,Object> sayPreHello(){
return helloWorldService.sayHello();
}
}
測試結果如下:
sayhello介面測試結果:
sayhello介面因為許可權為ALL,所以不需要userId都可以成功執行。
everyhello介面測試結果:
沒有userId
有userId
prehello介面測試結果:
通過上面的方式,我們就可以輕鬆建立一個屬於自己的許可權控制模組,因為不需要傳統session,而且採用的是AOP的方式進行開發,使得該功能更方便用於分散式系統中,而且沒有很多繁瑣的配置。
示例程式碼下載地址:http://download.csdn.net/download/u010520626/10267210