前言
最近工作中使用了公司sso鑒權,學習記錄下。
spring-security鑒權
spring-security的標准鑒權方式通過在controller方法上加@PreAuthorize("hasRole('ROLE_ADMIN')")
或@PreAuthorize("hasAuthority('admin')")
,最終鑒權是在SecurityExpressionRoot內進行鑒權,鑒權是通過判斷登錄用戶的權限集合Authentication.getAuthorities(),即注解內的key是否包含在用戶所攜帶的權限集合內。用戶的權限信息通常是permission表。
我們公司sso鑒權,鑒權時判斷當前用戶有沒有權限在方法上增加如下注解('can_add_user' 為權限 ) @PreAuthorize("@permissionService.hasPermission('can_add_user')")
,這個寫法有點奇怪,查了下@preAuthorize支撐spring EL表達式,@permissionService.hasPermission('can_add_user')是個表達式意思是引用bean permissionService的hasPermission方法,傳入參數是can_add_user。具體這種el用法見https://docs.spring.io/spring-framework/docs/5.2.6.RELEASE/spring-framework-reference/core.html#expressions-bean-references
以我司為例,用戶是通過sso進行統一認證,用戶權限信息配置在sso,用戶認證后的用戶是個實現了org.springframework.security.core.Authentication的認證用戶對象,攜帶了用戶基本信息和權限,那么通過 @PreAuthorize("@permissionService.hasPermission('can_add_user')")
這種方式進行鑒權實際上也是判斷用戶攜帶的權限是否包含can_add_user,包含則認為鑒權通過。用戶認證信息打印結構如下(permissions字段就是認證用戶權限集合)
{
dept_code=0605125, country=, city=上海市, emp_number=01876012,
user_code=12345, mobile_phone=15612345678,
permissions={"access_granted":{"scope_ctrl_type":"node","value":true,"node_scopes":[1666]},"can_add_user":{"value":true}},
....
}
spring-security鑒權源碼分析
那么@PreAuthorize是如何實現鑒權的呢?
通過web fitler方式?此時filter執行還不知道要調用的controller method,因此無法實現。
通過webmvc 攔截器HandlerInterceptor?可以通過HandlerMethod獲取目標method,從而判斷method上的注解進行鑒權,這種方式也可以。
通過啟動時候為@PreAuthorize注解的方法生成動態代理,controller類通常無接口,因此生成cglib動態代理類,通過反射調用獲取接口上的@PreAuthorize從而進行鑒權。
實際@PreAuthorize鑒權spring-security采用的是生成動態代理類,而非使用HandlerInterceptor,可能是HandlerInterceptor有攔截匹配問題吧。
鑒權執行堆棧如下
PermissionService.hasPermission(String) line: 33 //鑒權
GeneratedMethodAccessor2403.invoke(Object, Object[]) line: not available
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 45005
Method.invoke(Object, Object...) line: 498
ReflectiveMethodExecutor.execute(EvaluationContext, Object, Object...) line: 130
MethodReference.getValueInternal(EvaluationContext, Object, TypeDescriptor, Object[]) line: 111
MethodReference.access$000(MethodReference, EvaluationContext, Object, TypeDescriptor, Object[]) line: 54
MethodReference$MethodValueRef.getValue() line: 390
CompoundExpression.getValueInternal(ExpressionState) line: 90
CompoundExpression(SpelNodeImpl).getTypedValue(ExpressionState) line: 114
SpelExpression.getValue(EvaluationContext, Class<T>) line: 300 //el表達式,引用bean PermissionService
ExpressionUtils.evaluateAsBoolean(Expression, EvaluationContext) line: 26
ExpressionBasedPreInvocationAdvice.before(Authentication, MethodInvocation, PreInvocationAttribute) line: 59
PreInvocationAuthorizationAdviceVoter.vote(Authentication, MethodInvocation, Collection<ConfigAttribute>) line: 72
PreInvocationAuthorizationAdviceVoter.vote(Authentication, Object, Collection) line: 40 //投票
AffirmativeBased.decide(Authentication, Object, Collection<ConfigAttribute>) line: 63 //
MethodSecurityInterceptor(AbstractSecurityInterceptor).beforeInvocation(Object) line: 233
MethodSecurityInterceptor.invoke(MethodInvocation) line: 65
CglibAopProxy$CglibMethodInvocation(ReflectiveMethodInvocation).proceed() line: 186 //cglib動態代理類
CglibAopProxy$DynamicAdvisedInterceptor.intercept(Object, Method, Object[], MethodProxy) line: 688
ReleaseRecordController$$EnhancerBySpringCGLIB$$a34ad12.getMangoReleaseRecordList(HttpServletRequest, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, Integer, String) line: not available
GeneratedMethodAccessor2402.invoke(Object, Object[]) line: not available
DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 45005
Method.invoke(Object, Object...) line: 498 //反射調用
ServletInvocableHandlerMethod(InvocableHandlerMethod).doInvoke(Object...) line: 189
ServletInvocableHandlerMethod(InvocableHandlerMethod).invokeForRequest(NativeWebRequest, ModelAndViewContainer, Object...) line: 138
ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...) line: 102
RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 895
RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod) line: 800
RequestMappingHandlerAdapter(AbstractHandlerMethodAdapter).handle(HttpServletRequest, HttpServletResponse, Object) line: 87
DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse) line: 1038
鑒權的關鍵是MethodSecurityInterceptor,這個和spring-security的內部鑒權FilterSecurityInterceptor都是extends AbstractSecurityInterceptor,不同是FilterSecurityInterceptor是filter層,而MethodSecurityInterceptor是通過反射,執行的位置不同,都是進行鑒權。
那么MethodSecurityInterceptor是在什么時候創建的呢?通過查找在GlobalMethodSecurityConfiguration內創建bean MethodSecurityInterceptor,同時設置AccessDecisionManager、MethodSecurityMetadataSource
而配置類GlobalMethodSecurityConfiguration需要通過@EnableGlobalMethodSecurity進行開啟。
spring-security鑒權的使用
要使用@preAuthorize,需要@EnableGlobalMethodSecurity(prePostEnabled = true),表示使用spring-security的pre post注解,注解如下:
@postAuthorize:post是后置意思,在方法執行完畢后進行鑒權。
@PreAuthorize:用來控制一個方法是否能夠被調用。這個使用最多,注解在方法和類上,用於方法級別鑒權。
@PostAuthorize:在方法調用完成后進行權限檢查,它不能控制方法是否能被調用,只能在方法調用完成后檢查權限決定是否要拋出AccessDeniedException。很少使用。post是后置意思。
@PostAuthorize("returnObject.id%2==0")
public User find(int id) {
User user = new User();
user.setId(id);
return user;
}
代碼表示在方法find()調用完成后進行權限檢查,如果返回值的id是偶數則表示校驗通過,否則表示校驗失敗,將拋出AccessDeniedException。
使用@PreFilter和@PostFilter可以對集合類型的參數或返回值進行過濾。使用@PreFilter和@PostFilter時,Spring Security將移除使對應表達式的結果為false的元素。
@PostFilter("filterObject.id%2==0")
public List<User> findAll() {
List<User> userList = new ArrayList<User>();
User user;
for (int i=0; i<10; i++) {
user = new User();
user.setId(i);
userList.add(user);
}
return userList;
}
代碼表示對返回結果中id不為偶數的user進行移除。filterObject是使用@PreFilter和@PostFilter時的一個內置表達式,表示集合中的當前對象。當@PreFilter標注的方法擁有多個集合類型的參數時,需要通過@PreFilter的filterTarget屬性指定當前@PreFilter是針對哪個參數進行過濾的。
如下面代碼就通過filterTarget指定了當前@PreFilter是用來過濾參數ids的。
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
...
}
總結
spring-security鑒權,需要開啟@EnableGlobalMethodSecurity(prePostEnabled = true)引入MethodSecurityInterceptor,來實現對@PreAuthorize處理,鑒權就是通過判斷用戶攜帶的權限集合是否包含要調用方法上的權限。因此重點就是我們開發的權限判斷了。
鑒權PermissionServer代碼記錄,方便使用
import com.yhd.common.enums.ExceptionEnum;
import com.yhd.common.exception.ErrorCodeException;
import com.yhd.common.util.StringUtil;
import com.yhd.web.filter.User;
import org.springframework.security.core.context.SecurityContextHolder;
import java.util.Set;
/**
* 權限鑒權service
*/
public class PermissionService {
/**
* 判斷是否有權限
* @param key sso權限key
* @return
*/
public boolean hasPermission(String key) {
return getPermission(key,Boolean.class);
}
/**
* 判斷權限配置的值是否等於指定的int值,可以是方法的參數,例如#user.id
* @param key
* @param value
* @return
*/
public boolean permissionValueEqualTo(String key,Integer value){
Integer v = getIntPermission(key);
if(v==null){
return false;
}
return v.equals(value);
}
/**
* 判斷權限配置的值是否等於指定的string值,可以是參數,例如#user.code
* @param key
* @param value
* @return
*/
public boolean permissionValueEqualTo(String key,String value){
String v = getStringPermission(key);
if(v==null){
return false;
}
return v.equals(value);
}
/**
* 判斷權限配置的值是否大於指定的int值,可以是參數,例如#user.id
* @param key
* @param value
* @return
*/
public boolean permissionValueGreaterThan(String key,Integer value){
Integer v = getIntPermission(key);
if(v==null){
return false;
}
return v.compareTo(value)>0;
}
/**
* 判斷權限配置的值是否大於等於指定的int值,可以是參數,例如#user.id
* @param key
* @param value
* @return
*/
public boolean permissionValueGreaterThanOrEqualTo(String key,Integer value){
Integer v = getIntPermission(key);
if(v==null){
return false;
}
return v.compareTo(value)>=0;
}
/**
* 判斷權限配置的值是否小於指定的int值,可以是參數,例如#user.id
* @param key
* @param value
* @return
*/
public boolean permissionValueLessThan(String key,Integer value){
Integer v = getIntPermission(key);
if(v==null){
return false;
}
return v.compareTo(value)<0;
}
/**
* 判斷權限配置的值是否小於等於指定的int值,可以是參數,例如#user.id
* @param key
* @param value
* @return
*/
public boolean permissionValueLessThanOrEqualTo(String key,Integer value){
Integer v = getIntPermission(key);
if(v==null){
return false;
}
return v.compareTo(value)<=0;
}
/**
* 判斷權限配置的值是否以指定的string值開頭,可以是參數,例如#user.name
* @param key
* @param value
* @return
*/
public boolean permissionValueStartsWith(String key,String value){
String v = getStringPermission(key);
if(v==null|| StringUtil.isEmpty(value)){
return false;
}
return v.startsWith(value);
}
/**
* 判斷權限配置的值是否以指定的string值結尾,可以是參數,例如#user.name
* @param key
* @param value
* @return
*/
public boolean permissionValueEndsWith(String key,String value){
String v = getStringPermission(key);
if(v==null||StringUtil.isEmpty(value)){
return false;
}
return v.endsWith(value);
}
/**
* 判斷數據權限配置的值是否包含指定的string值,可以是參數,例如#user.name
* @param key
* @param value
* @return
*/
public boolean permissionValueContains(String key,String value){
Set<String> v = getDataPermission(key);
if(v==null||StringUtil.isEmpty(value)){
return false;
}
return v.contains(value);
}
public Integer getIntPermission(String key){
return getPermission(key,Integer.class);
}
public String getStringPermission(String key){
return getPermission(key,String.class);
}
public Set<String> getDataPermission(String key){
return getPermission(key,Set.class);
}
public <T> T getPermission(String key,Class<T> t){
User user = (User) SecurityContextHolder.getContext().getAuthentication();
if (user == null) {
return null;
}
if(t.isAssignableFrom(Boolean.class)){
if(user.getPermissions()==null){
return (T)Boolean.valueOf(false);
}else{
return (T)Boolean.valueOf(user.getPermissions().contains(key));
}
}else if(t.isAssignableFrom(Integer.class)){
if(user.getIntPermissions()==null){
return null;
}else{
return (T)user.getIntPermissions().get(key);
}
}else
if( t.isAssignableFrom(String.class)){
if(user.getStringPermissions()==null){
return null;
}else{
return (T)user.getStringPermissions().get(key);
}
}else if(t.isAssignableFrom(Set.class)){
if(user.getDataPermissions()==null){
return null;
}else{
return (T)user.getDataPermissions().get(key);
}
}else{
throw new ErrorCodeException(ExceptionEnum.PARAMETER_EXCEPTION,"權限獲取","未知的類型"+t+",請選擇[Boolean.class,String.class,Integer.class,Set.class]");
}
}
}
疑問:
在什么時候對@PreAuthorize的方法類生成動態代理類的呢?流程有些復雜,后續回顧下aop代理類生成過程。