最近在使用spring security进行编码,在实际使用的过程中,遇到的问题记录一下。
背景:在一个项目中,我使用spring security进行权限控制。不仅前台控制页面和按钮的显示,还在后台对没有权限的请求进行过滤。因为每个需要进行权限控制的后台请求,都需要写相同的代码,如果一个两个还好,写多了就开始想能不能减少代码量。
先看看没有使用自定义注解的时候是怎么在后台进行权限控制的
上面的例子是一个上传文件后台请求,我们需要判断这个用户是否有上传文件的权限。在这里我定义了一个工具类AuthenticationUtil,用来判断用户有没有登录(authen),和判断用户是否具有某个权限(authenRole),并把判断结果放到Map中。如果没有权限,就不再执行后面的语句。假如我每个请求都需要判断权限,那么每个请求的开头都需要加这样5行代码,这样代码重用性就低。而且controller里面不能专注关心业务,还混入了权限的代码。
为了解决这个问题,使用自定义注解,插入一个AOP,可以很好的解决这个问题。
看一下使用AOP之后的代码
一行代码就完成之前5行代码做的事情,是不是很简洁,controller中也不用再关心权限的问题了。
这里我使用Springboot中的Aop技术,定义一个切面,使用around进行注入。
1.自定义注解AuthenticRequire
package demo.config.aop;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticRequire {
//权限的名称
String value() default "";
//没有权限的时候,通过这个接口得到返回值
Class extends IReturnHandle> handle() default DefaultReturnHandle.class;
}
value是权限的名称,而另一个变量handle,是用来进行类型转换的,因为有时候url请求的返回值不是Map,而是其他类型,这时候就需要自己实现一个类型转换类来完成返回值的转换。
2.定义类型转换接口IReturnHandle
package demo.config.aop;
public interface IReturnHandle {
//完成类型转换,从类型T转成类型K,和java8的内置函数apply一样的作用
K handle(T t);
}
3.定义一个默认的类型转换实现类
package demo.config.aop;
import java.util.Map;
public class DefaultReturnHandle implements IReturnHandle
4.针对AuthenticRequire注解,定义一个AOP切面,完成权限控制
package demo.config.aop;
import demo.util.AuthenticationUtil;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Aspect
@Component
@Log4j2
public class AuthenticAop {
@Pointcut("@annotation(demo.config.aop.AuthenticRequire)")
public void operationLog(){}
@Around("operationLog()")
public Object around(ProceedingJoinPoint pjp){
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
AuthenticRequire authenticRequire = method.getAnnotation(AuthenticRequire.class);
Map result = new HashMap<>();
boolean authen = false;
Object[] args = pjp.getArgs();
if(args!=null){
//从方法的参数中,过滤出HttpServletRequest参数
Optional
5.最后直接使用就可以了,在有需要的地方注入注解,完成权限控制。假如用户没有对应权限,则请求不会进入到controller中。
如果方法返回值是Map
因为下载文件的请求的返回值是ResponseEntity,所以需要自己实现一个IReturnHandle实现类进行类型转换。
注意:注解的方法需要有一个参数HttpServletRequest参数,用来获取security权限信息。如果没有这个参数,则认为没有权限。
最后再付上权限判断的工具类
package demo.util;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
import java.util.Map;
/**
* 权限认证操作
*/
public class AuthenticationUtil {
/**
* 判断用户是否登录
* @param principal
* @param result
* @return
*/
public static boolean authen(AbstractAuthenticationToken principal, Map result){
if(principal==null) {
result.put("error", "用户未登陆");
return false;
}
return true;
}
/**
* 判断用户是否具备某个权限
* @param principal
* @param roleName
* @param result
* @return
*/
public static boolean authenRole(AbstractAuthenticationToken principal, String roleName, Map result){
if(roleName==null || roleName.isEmpty())
return true;
boolean isRole = principal.getAuthorities().stream()
.map(o->o.getAuthority()).filter(o->o.equalsIgnoreCase(roleName)).count()>0;
if(!isRole){
if(result!=null)
result.put("error", "当前用户没有操作权限");
return false;
}
return true;
}
}
因为项目涉及其他代码,就不上传了。