【Spring Security】使用 API 进行鉴权实现

1,531 阅读4分钟

前言

在使用Spring Security进行鉴权时,可以在方法上使用@PreAuthorize()等注解很方便的进行鉴权。然而最近在开发门面(facade)模块的时候,有的REST接口需要整合多个RPC接口,比如POST /users (用于注册账号)有两种方式,一种是账号密码(不对外开放,需要特殊的admin权限),一种是短信验证码方式(对外开放,不需要权限)。这时,更希望能够使用API的方式进行鉴权(所谓API方式就是通过调用方法的方式)。然而,Spring Security默认并不支持API的方式进行鉴权,因此我们需要进行自定义。

API鉴权切入点

通过查阅源码得知,Spring Security注解的鉴权主要是通过MethodSecurityExpressionRoot类进行,该类继承自SecurityExpressionRoot,实现了各种鉴权操作如hasAuthorize()hasRole()permitAll()等,因此我们只要使用该类即可进行API鉴权。因为该类是抽象类,因此我们继承该类即可。

实现方式1-继承SecurityExpressionRoot实现SecurityAuthorize类

我们首先想到的是简单的继承SecurityExpressionRoot然后直接使用。

实现SecurityExpressionRoot

public class SecurityAuthorize extends SecurityExpressionRoot {
    /**
     * Creates a new instance
     *
     * @param authentication the {@link Authentication} to use. Cannot be null.
     */
    public SecurityAuthorize(Authentication authentication) {
        super(authentication);
    }
}

使用方式:

    @GetMapping
    public Object getUser(Authentication authentication) {
        SecurityAuthorize securityAuthorize = new SecurityAuthorize(authentication);
        securityAuthorize.setRoleHierarchy(roleHierarchy);
        if (securityAuthorize.hasRole("admin")) {

        }
        return "user";
    }

实现方式2-构造SecurityAuthorizeFactory

上面的实现方式我们每次使用都需要在方法的参数里添加Authentication,而且需要自己添加需要的参数,如RoleHierarchy。这种方式并不够优雅,因此我们构造工厂,让工厂去帮我们添加Authentication和需要的其他参数。

实现SecurityAuthorizeFactory

/**
 * 描述:自定义鉴权工厂
 *
 * @author xhsf
 * @create 2020/11/29 0:36
 */
@Component
public class SecurityAuthorizeFactory {

    private final RoleHierarchy roleHierarchy;

    public SecurityAuthorizeFactory(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    public SecurityAuthorize instance() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SecurityAuthorize securityAuthorize = new SecurityAuthorize(authentication);
        securityAuthorize.setRoleHierarchy(roleHierarchy);
        return securityAuthorize;
    }
}

使用方式:

    @GetMapping
    public Object getUser() {
        if (securityAuthorizeFactory.instance().hasRole("admin")) {

        }
        return "user";
    }

实现方式3-使用AOP

上面使用工厂的方式是一种已经非常优雅的方式了,然而有时候我们更加希望像其他的参数一样(如requestsessionauthentication)等,在我们需要的时候添加在方法上即可使用。因此我们通过AOP的方式实现该方式。

实现SecurityAuthorizeAspect

这里判断是否有SecurityAuthorize类型参数添加参数,并使用@Before()切入到需要的地方。

/**
 * 描述:SecurityAuthorize的切面
 *      用于把SecurityAuthorize注入到需要该对象的方法里
 *      只需要在方法添加SecurityAuthorize参数即可
 *
 * @author xhsf
 * @create 2020/11/29 13:23
 */
@Aspect
@Component
public class SecurityAuthorizeAspect {
    private final RoleHierarchy roleHierarchy;

    public SecurityAuthorizeAspect(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }

    @Before(value = "within(com.xiaohuashifu.recruit.facade.service.controller.v1.*)")
    public void securityAuthorize(JoinPoint joinPoint) {
        // 判断该方法参数是否带有SecurityAuthorize,有的话帮忙插入需要的参数
        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if (arg instanceof SecurityAuthorize) {
                Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
                ((SecurityAuthorize) arg).setAuthentication(authentication);
                ((SecurityAuthorize) arg).setRoleHierarchy(roleHierarchy);
                break;
            }
        }
    }
}

实现带空参数构造器的SecurityAuthorize

因为该方式需要使用无参数的构造器,不然Spring MVC在创建SecurityAuthorize时会因为传入的Authentication==null而报错。由于SecurityExpressionRoot在构造的时候必须传入Authentication,因此我们不能再继续继承SecurityExpressionRoot,而是应该复制它的代码,然后添加无参构造器setAuthentication()方法。

/**
 * 描述:自定义SecurityExpressionRoot,可以使用API进行鉴权
 *      如hasRole(),hasAnyAuthority()
 *      改造自SecurityExpressionRoot
 *
 * @author xhsf
 * @create 2020/11/28 21:53
 */
public class SecurityAuthorize implements SecurityExpressionOperations {
    // 这里去掉原来的final,否则无法添加无参构造器
    protected Authentication authentication;
    
    // other codes

    // 无参构造器
    public SecurityAuthorize() {
    }
	
    // 添加setAuthentication()方法
    public void setAuthentication(Authentication authentication) {
        this.authentication = authentication;
    }
    
    // other codes
}

使用方式:

    @GetMapping
    public Object getUser(SecurityAuthorize securityAuthorize) {
        if (securityAuthorize.hasRole("admin")) {

        }
        return "user";
    }

举个例子

使用@PreAuthorize()注解

    @PostMapping
    @PreAuthorize("(#params.get('type').equals('sms')) " +
            "or (#params.get('type').equals('password') and hasRole('admin'))")
    public Object post(@RequestBody Map<String, String> params) {
        if (params.get("type").equals("sms")) {
            return userService.signUpBySmsAuthCode(
                    params.get("phone"), params.get("authCode"), params.get("password"));
        }
        if (params.get("type").equals("password")) {
            return userService.signUpUser(params.get("username"), params.get("password"));
        }
        return Result.fail(ErrorCode.INVALID_PARAMETER);
    }

使用API

    @PostMapping
    public Object post(SecurityAuthorize securityAuthorize, 
                       @RequestBody Map<String, String> params) {
        if (params.get("type").equals("sms")) {
            return userService.signUpBySmsAuthCode(
                    params.get("phone"), params.get("authCode"), params.get("password"));
        }
        if (params.get("type").equals("password") && securityAuthorize.hasRole("admin")) {
            return userService.signUpUser(params.get("username"), params.get("password"));
        }
        return Result.fail(ErrorCode.INVALID_PARAMETER);
    }

最后

其实这里只是提供了一种API的方式进行鉴权而已,并没有说哪种方式更好,可以注解和API两种方式一起使用。