首页 > 技术文章 > spring-security鉴权随笔

zhangyjblogs 2021-03-08 00:32 原文

前言

最近工作中使用了公司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

image-20210307230838945

而配置类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代理类生成过程。

推荐阅读