前言
在使用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
上面使用工厂的方式是一种已经非常优雅的方式了,然而有时候我们更加希望像其他的参数一样(如request,session,authentication)等,在我们需要的时候添加在方法上即可使用。因此我们通过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两种方式一起使用。