Spring Security @PreAuthorization 直接传入枚举

Posted

技术标签:

【中文标题】Spring Security @PreAuthorization 直接传入枚举【英文标题】:Spring Security @PreAuthorization pass enums in directly 【发布时间】:2013-10-18 16:11:26 【问题描述】:

我的问题与Custom annotation with spring security 重复,但没有得到解答,我相信应该有一个简单的解决方案。

基本上不用做:

@PreAuthorize("hasPermission(T(fully.qualified.Someclass).WHATEVER, T(fully.qualified.Permission).READ")

我想做:

@PreAuthorize(Someclass.WHATEVER, Permission.READ)

或者可能是一些自定义注释,可以很容易地与 spring security 连接起来

这对我来说似乎更干净,如果可以的话,我希望能够做到。

【问题讨论】:

运气好能解决这个问题吗?我今天也有同样的问题。 不,我硬着头皮用了字符串=( 【参考方案1】:

确实,您可以实现自定义的强类型安全注释,尽管这很麻烦。 声明你的注释

enum Permission 
    USER_LIST,
    USER_EDIT,
    USER_ADD,
    USER_ROLE_EDIT


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Permissions 
    Permission[] value();

声明安全管道使用的org.springframework.security.access.ConfigAttribute的自定义实现

class SecurityAttribute implements ConfigAttribute 
    private final List<Permission> permissions;

    public SecurityAttribute(List<Permission> permissions) 
        this.permissions = permissions;
    

    @Override
    public String getAttribute() 
        return permissions.stream().map(p -> p.name()).collect(Collectors.joining(","));
    

声明org.springframework.security.access.method.MethodSecurityMetadataSource的自定义实现,通过注解创建SecurityAttribute的实例

class SecurityMetadataSource extends AbstractMethodSecurityMetadataSource 
    @Override
    public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) 

      //consult https://github.com/spring-projects/spring-security/blob/master/core/src/main/java/org/springframework/security/access/prepost/PrePostAnnotationSecurityMetadataSource.java
      //to implement findAnnotation  
      Permissions annotation = findAnnotation(method, targetClass, Permissions.class);
        if (annotation != null) 
            return Collections.singletonList(new SecurityAttribute(asList(annotation.value())));
        
        return Collections.emptyList();
    

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() 
        return null;
     


最后声明自定义实现org.springframework.security.access.AccessDecisionVoter

public class PermissionVoter implements AccessDecisionVoter<MethodInvocation> 
    @Override
    public boolean supports(ConfigAttribute attribute) 
        return attribute instanceof SecurityAttribute;
    

    @Override
    public boolean supports(Class<?> clazz) 
        return MethodInvocation.class.isAssignableFrom(clazz);
    

    @Override
    public int vote(Authentication authentication, MethodInvocation object, Collection<ConfigAttribute> attributes) 
        Optional<SecurityAttribute> securityAttribute = attributes.stream()
                .filter(attr -> attr instanceof SecurityAttribute).map(SecurityAttribute.class::cast).findFirst();
        if(!securityAttribute.isPresent())
            return AccessDecisionVoter.ACCESS_ABSTAIN;
        
        //authorize your principal from authentication object
        //against permissions and return ACCESS_GRANTED or ACCESS_DENIED

    


现在把它们放在你的MethodSecurityConfig

@Configuration
@EnableGlobalMethodSecurity
class MethodSecurityConfig extends GlobalMethodSecurityConfiguration 

    @Override
    protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() 
        return new ScpSecurityMetadataSource();
    

    @Override
    protected AccessDecisionManager accessDecisionManager() 
        return new AffirmativeBased(Collections.singletonList(new PermissionVoter()));
    

【讨论】:

虽然确实不是“一条龙”的神奇答案,但还是提出了添加自定义安全注解的绝佳方案!我只是按照实现逻辑,不难理解整体逻辑。【参考方案2】:

面对同样的问题,我最终选择了一个混合解决方案。我正在使用 Spring-El 和自定义 bean 来提供我自己的接受枚举的 hasPermission() 方法。鉴于 Spring 会自动进行 string-&gt;enum 转换,在运行时,如果字符串中存在拼写错误,我将得到一个特定枚举不存在的运行时异常。不是理想的解决方案(宁愿有一些在编译时失败的东西),而是一个可以接受的折衷方案。它给了我一些半安全性。

@Component("securityService")
public class SecurityService 
    public boolean hasPermission( Permission...permissions)
        // loop over each submitted role and validate the user has at least one
        Collection<? extends GrantedAuthority> userAuthorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
        for( Permission permission : permissions)
            if( userAuthorities.contains( new SimpleGrantedAuthority(permission.name())))
                return true;
        

        // no matching role found
        return false;
    

如下使用:

@PreAuthorize("@securityService.hasPermission('USER_ADD')")
public User addUser(User user)
    // create the user
    return userRepository.save( user );

Permission 只是一个普通的枚举定义:

public enum Permission 
    USER_LIST,
    USER_EDIT,
    USER_ADD,
    USER_ROLE_EDIT

希望这可以帮助将来的其他人。

【讨论】:

【参考方案3】:

您可以像这样创建静态注释:

@ReadPermission

通过将@PreAuthorize 注释移动到@ReadPermissiondefinition:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole(T(fully.qualified.Permission).READ.roleName())")
public @interface ReadPermission 
    

这样做的好处是,您可以在一个地方更改 Spring SPEL 表达式,而不是在每个方法上都修改它。

另外一个优点是,您可以在类级别使用此注解 - 然后每个方法都将使用此注解进行保护。它对 AdminControllers 等很有用。

【讨论】:

好像spring security会忽略这些自定义注解 好像@Retention(RetentionPolicy.RUNTIME) 不见了,现在好像可以了【参考方案4】:

我就是这样做的:

1 - 定义你的枚举引用一个公共的最终静态字符串“VALUE”,像这样

public enum MyEnum 
    ENUM_A(Names.ENUM_A);

    private String value;

    private MyEnum (String value) 
        this.value = value;
    

    public static class Names 

        public  final static String ENUM_A = "ENUM_A";
    

2 - 在@PreAuthorize 中连接 MyEnum 值

@PreAuthorize("hasPermission('myDomain', '"+ MyEnum.Names.ENUM_A+"')")

【讨论】:

当我尝试这种方式时,我得到一个“属性值必须是常量”的编译错误。你解决了吗? 如果与 MyEnum.Names.ENUM_A 连接会出现编译错误?您是否将其定义为示例中的公共最终静态字符串? 这不是没有抓住重点,我只想定义我的角色名称一次吗?我必须手动更新静态名称 这样你只能在枚举中定义一次【参考方案5】:

我创建了自己的注释,它在参数中获取枚举。在实现注解时,我创建了一个方法来获取注解中指定的所有角色并验证当前用户是否至少拥有其中一个。如果没有一个角色匹配,程序将抛出异​​常。

枚举:

public enum MyRoles 
    ADMIN("ROLE_ADMIN"),
    USER("ROLE_USER"),
    GUEST("ROLE_GUEST");

    private String name;

    private MyRoles(String name) 
        this.name = name;
    

注解接口:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AllowedRoles 
    MyRoles[] value();

注释的实现:

@Aspect
@Component
public class AllowedRolesAspect 
    @Around("@annotation(com.myproject.annotations.AllowedRoles)")
    public Object doSomething(ProceedingJoinPoint jp) throws Throwable 

        Set<MyRoles> roles = Arrays.stream(((MethodSignature) jp.getSignature()).getMethod()
                .getAnnotation(AllowedRoles.class).value()).collect(Collectors.toSet());

        HttpServletRequest httpServletRequest = getRequest();

        for(MyRoles role : roles)
            if(httpServletRequest.isUserInRole(role))
                return jp.proceed();
            
        

        throw new AccessDeniedException("");
    

    private HttpServletRequest getRequest() 

        ServletRequestAttributes servletRequestAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        return servletRequestAttributes.getRequest();
    

用法:

@AllowedRoles(MyRoles.ADMIN, MyRoles.USER)
@GetMapping("/myrequest/id")
public MyResponse getResponse(
        @PathVariable Long id
) 
    /*Do something...*/

【讨论】:

以上是关于Spring Security @PreAuthorization 直接传入枚举的主要内容,如果未能解决你的问题,请参考以下文章

Spring mvc / security:从spring security中排除登录页面

Spring Security:2.4 Getting Spring Security

没有 JSP 的 Spring Security /j_spring_security_check

Spring-Security

Spring Security 登录错误:HTTP 状态 404 - /j_spring_security_check

未调用 Spring Security j_spring_security_check