最近在使用spring security進行編碼,在實際使用的過程中,遇到的問題記錄一下。
背景:在一個項目中,我使用spring security進行權限控制。不僅前臺控制頁面和按鈕的顯示,還在後臺對沒有權限的請求進行過濾。因爲每個需要進行權限控制的後臺請求,都需要寫相同的代碼,如果一個兩個還好,寫多了就開始想能不能減少代碼量。
先看看沒有使用自定義註解的時候是怎麼在後臺進行權限控制的
上面的例子是一個上傳文件後臺請求,我們需要判斷這個用戶是否有上傳文件的權限。在這裏我定義了一個工具類AuthenticationUtil,用來判斷用戶有沒有登錄(authen),和判斷用戶是否具有某個權限(authenRole),並把判斷結果放到Map中。如果沒有權限,就不再執行後面的語句。假如我每個請求都需要判斷權限,那麼每個請求的開頭都需要加這樣5行代碼,這樣代碼重用性就低。而且controller裏面不能專注關心業務,還混入了權限的代碼。
爲了解決這個問題,使用自定義註解,插入一個AOP,可以很好的解決這個問題。
看一下使用AOP之後的代碼
一行代碼就完成之前5行代碼做的事情,是不是很簡潔,controller中也不用再關心權限的問題了。
這裏我使用Springboot中的Aop技術,定義一個切面,使用around進行注入。
1.自定義註解AuthenticRequire
package demo.config.aop;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticRequire {
//權限的名稱
String value() default "";
//沒有權限的時候,通過這個接口得到返回值
Class<? extends IReturnHandle> handle() default DefaultReturnHandle.class;
}
value是權限的名稱,而另一個變量handle,是用來進行類型轉換的,因爲有時候url請求的返回值不是Map,而是其他類型,這時候就需要自己實現一個類型轉換類來完成返回值的轉換。
2.定義類型轉換接口IReturnHandle
package demo.config.aop;
public interface IReturnHandle<T,K> {
//完成類型轉換,從類型T轉成類型K,和java8的內置函數apply一樣的作用
K handle(T t);
}
3.定義一個默認的類型轉換實現類
package demo.config.aop;
import java.util.Map;
public class DefaultReturnHandle implements IReturnHandle<Map<String, Object>, Map<String, Object>> {
@Override
public Map<String, Object> handle(Map<String, Object> stringStringMap) {
return stringStringMap;
}
}
4.針對AuthenticRequire註解,定義一個AOP切面,完成權限控制
package demo.config.aop;
import demo.util.AuthenticationUtil;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Aspect
@Component
@Log4j2
public class AuthenticAop {
@Pointcut("@annotation(demo.config.aop.AuthenticRequire)")
public void operationLog(){}
@Around("operationLog()")
public Object around(ProceedingJoinPoint pjp){
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
AuthenticRequire authenticRequire = method.getAnnotation(AuthenticRequire.class);
Map<String, Object> result = new HashMap<>();
boolean authen = false;
Object[] args = pjp.getArgs();
if(args!=null){
//從方法的參數中,過濾出HttpServletRequest參數
Optional<Object> objectOptional = Arrays.stream(args).filter(arg->arg instanceof HttpServletRequest).findFirst();
if(objectOptional.isPresent()) {
HttpServletRequest request = (HttpServletRequest) objectOptional.get();
//判斷用戶有沒有登錄,如果有登錄,再判斷有沒有權限
authen = AuthenticationUtil.authen((AbstractAuthenticationToken) request.getUserPrincipal(), result);
if(authen) {
String roleName = authenticRequire.value();
authen = AuthenticationUtil.authenRole((AbstractAuthenticationToken) request.getUserPrincipal(), roleName, result);
}
}
}
//如果沒有權限,直接返回
if(!authen){
if(authenticRequire.handle()!=null){
try {
IReturnHandle<Map<String, Object>,?> handle = authenticRequire.handle().newInstance();
return handle.handle(result);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
return result;
}
//有相應的權限,執行方法,並返回結果
Object methodReturn = null;
try {
methodReturn = pjp.proceed();
} catch (Throwable throwable) {
log.error(throwable.getMessage(), throwable);
}
return methodReturn;
}
}
5.最後直接使用就可以了,在有需要的地方注入註解,完成權限控制。假如用戶沒有對應權限,則請求不會進入到controller中。
如果方法返回值是Map<String,Object>,那就這樣就行,如果不是,則需要多傳遞一個變量,如下圖
因爲下載文件的請求的返回值是ResponseEntity,所以需要自己實現一個IReturnHandle實現類進行類型轉換。
注意:註解的方法需要有一個參數HttpServletRequest參數,用來獲取security權限信息。如果沒有這個參數,則認爲沒有權限。
最後再付上權限判斷的工具類
package demo.util;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Map;
/**
* 權限認證操作
*/
public class AuthenticationUtil {
/**
* 判斷用戶是否登錄
* @param principal
* @param result
* @return
*/
public static boolean authen(AbstractAuthenticationToken principal, Map<String, Object> result){
if(principal==null) {
result.put("error", "用戶未登陸");
return false;
}
return true;
}
/**
* 判斷用戶是否具備某個權限
* @param principal
* @param roleName
* @param result
* @return
*/
public static boolean authenRole(AbstractAuthenticationToken principal, String roleName, Map<String, Object> result){
if(roleName==null || roleName.isEmpty())
return true;
boolean isRole = principal.getAuthorities().stream()
.map(o->o.getAuthority()).filter(o->o.equalsIgnoreCase(roleName)).count()>0;
if(!isRole){
if(result!=null)
result.put("error", "當前用戶沒有操作權限");
return false;
}
return true;
}
}
因爲項目涉及其他代碼,就不上傳了。