使用场景:
基本上每个项目都离不开权限设计,这里我研究了下XX的基础框架,理解了aop权限在项目中的真实场景及思路
代码描述:
``
自定义认证+权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
/**
* 该属性是用来校验权限的
*/
boolean verify() default true;
/**
* 该属性可加可不加,可以扩展为接口的自定义权限值
*/
//String value();
}
``
自定义切面类
@Slf4j
@Aspect
@Component
public class AuthAop {
@Autowired
private TokenService tokenService;
/**
*
* @param jp
* @param aaa 变量名可随意,可以理解为一个注解类型的对象
*/
@Before("@annotation(aaa)")
public void auth(JoinPoint jp,Auth aaa){
//封装的工具类,基于ThreadLocal获取当前请求
HttpServletRequest httpServletRequest = GlobalParamUtils.currentRequest();
// 获取请求的地址,即是协议+ip+端口后面的资源路径
String requestURI = httpServletRequest.getRequestURI();
// 利用注解@Slf4j打印信息
log.info("请求url:{}",requestURI);
// 获取请求头
String token = httpServletRequest.getHeader("token");
// 通过断言工具类检验token是否为null,为null就抛出异常,等待全局异常捕获处理
AssertUtil.hasText(token, TokenExceptionEnum.TOKEN_NONE);
// 根据token从redis中获取authInfo,里面包含了对token的判断
AuthInfoVO authInfo = tokenService.userInfoFromToken(token);
// 断言判断是否为null,为null就抛出异常,等待全局异常捕获处理
AssertUtil.notNull(authInfo, TokenExceptionEnum.TOKEN_EXPIRE);
// 根据请求参数,来决定是否设置authInfo
boolean flag = setTokenInfo(jp, authInfo);
// 如果参数中没有BaseAuthVO入参,或者没有AuthInfoVO类型的属性,那就将authInfoVO存入该线程
if (!flag) {
log.info("由于没有认证参数,我需要将authInfoVO存入该线程");
// TokenInfoHolder下面会说
TokenInfoHolder.set(authInfo);
}
// 判断是否需要权限校验,跟请求uri比较,如果没有权限抛出权限异常,然后捕获返回
if (aaa.verify()) {
// 从redis中根据用户id取出权限列表,权限信息不会存在token或者authInfo中
Set<String> functions = tokenService.getFunctions(authInfo.getUserId());
log.info("用户功能权限有:{}",functions);
String authStr = requestURI.toLowerCase();
log.info(authStr);
// 判断权限集合中是否有该请求路径,数据库中存入的就是请求uri
AssertUtil.contain(functions, authStr, TokenExceptionEnum.MEMBER_TYPE_INVALIDATE);
}
// 刷新redis中token为1小时,即重新设置时间
tokenService.renewalToken(token);
}
/**
* 看请求参数中是否有基础认证入参,如果有,就给该参数设置authInfo,
* 注意:无论如何,当前访问的用户信息都会存储,要么存在入参参数中,要么存在线程中,
* 目的就是为了保证在数据库操作时能记录用户信息
* @param jp
* @param authInfo
* @return
*/
private boolean setTokenInfo(JoinPoint jp, AuthInfoVO authInfo) {
// 获取被拦截请求的参数列表
Object[] args = jp.getArgs();
boolean flag = false;
log.info("用户ID[{}]", authInfo.getUserId());
// 如果是文件参数类型,或者HttpServletRequest|HttpServletResponse就跳过当前循环
for (Object arg : args) {
if (arg instanceof MultipartFile
|| arg instanceof HttpServletRequest
|| arg instanceof HttpServletResponse) {
continue;
}
log.info("请求参数{}", arg);
// BaseAuthVO交互类只有一个AuthInfoVO类属性
if (arg instanceof BaseAuthVO) {
BaseAuthVO reqVO = (BaseAuthVO) arg;
reqVO.setAuthInfoVO(authInfo);
flag = true;
} else {
if (Objects.isNull(arg)) {
continue;
}
// 比如其他的对象继承了BaseAuthVO,因此也有这个AuthInfoVO类属性,
// 于是就可以遍历字段属性,看字段的类型是不是AuthInfoVO类,
// 如果有,就将从redis取出来的authInfo设置到该字段上,注意:
// 发送该请求时的携带的@RequestBody参数不用带上authInfoVO喔,
// 因为是反射技术,他可以拿到该类的所有属性的
Class<?> clazz = arg.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (Objects.equals(field.getGenericType().toString(), "class com.shidai.vo.AuthInfoVO")) {
field.setAccessible(true);
try {
field.set(arg, authInfo);
flag = true;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
return flag;
}
/**
* 通过ThreadLocal.remove()清除,防止内存溢出
*/
@After(value = "@annotation(auth)")
public void clearSuccess(Auth auth) {
TokenInfoHolder.clear();
}
/**
* 通过ThreadLocal.remove()清除,防止内存溢出
*/
@AfterThrowing(value = "@annotation(auth)")
public void clearError(Auth auth) {
TokenInfoHolder.clear();
}
}
/**
* TokenInfoHolder----就是基于ThreadLocal存储用户信息的,保证了线程安全
*
* @author roger
* @date 2021/6/29 13:48
*/
public class TokenInfoHolder {
/**
* 一个线程内部共享,不同线程之间是隔离的,每个线程只能get()到自己的变量
* ThreadLocal保证了每个线程都有一个独立实例副本,避免线程安全问题
* 一个线程可以有多个TreadLocal来存放不同类型的对象的,都将放到你当前线程的ThreadLocalMap(threadLocals)-是一个数组结构
* Entry[] ThreadLocal - k; Object - v
* 应用场景:spring的事务管理用过(保证拿到同一个连接对象);还有springmvc的RequestContextHolder,因此你在aop中业务中都能拿到安全的对应请求对象
*
*/
private static final ThreadLocal<String> AUTH = new ThreadLocal<>();
private TokenInfoHolder() {
}
/**
* 将authInfo转换成JSon String存储
*
* @param authInfo 前端传入的AuthInfoVO对象
* 设置到线程中的ThreadLocalMaps中的ThreadLocal(AUTH)对象中
*/
public static void set(AuthInfoVO authInfo) {
String json = JSON.toJSONString(authInfo);
AUTH.set(json);
}
/**
* 通过ThreadLocal.get()从获取AuthInfo对象
*
* @return
*/
public static AuthInfoVO authInfo() {
return authInfo(AuthInfoVO.class);
}
private static <T> T authInfo(Class<T> clazz) {
String info = AUTH.get();
return JSON.parseObject(info, clazz);
}
/**
* 通过ThreadLocal.remove()清除,防止内存溢出
*/
public static void clear() {
AUTH.remove();
}
}
``
登录认证
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Resource
UserMapper userMapper;
@Resource
RoleMapper roleMapper;
@Resource
PermissionMapper permissionMapper;
@Resource
TokenService tokenService;
@Override
public BaseResultVO<UserPO> login(AuthInfoVO auth) {
LambdaQueryWrapper<UserPO> lambdaQueryWrapper = new LambdaQueryWrapper<UserPO>().eq(UserPO::getUserName, auth.getUsername());
UserPO user = userMapper.selectOne(lambdaQueryWrapper);
AssertUtil.notNull(user,"该用户不存在");
log.info("用户信息为:{}",user);
auth.setUserId(String.valueOf(user.getUserId()));
// 查询到所属角色
String rid= roleMapper.getRoleListById(user.getUserId());
// 根据角色再去查询所有的权限,存入redis中
Set<String> func = permissionMapper.funcAuth(Integer.parseInt(rid));
tokenService.setFunctions(String.valueOf(user.getUserId()),func);
// 生成token,存入缓存,然后返回给前端
String token = tokenService.setTokenInfo(auth);
// 获取响应对象,设置token
HttpServletResponse response = GlobalParamUtils.currentResponse();
response.setHeader("token",token);
return BaseResultVO.success("登录认证成功");
}
}
图象辅助:
登录后redis中,用户信息就是token值
总结思路:
1.用户登录是放行不加认证权限校验的
2.登陆时做两件事:a.通过工具类根据用户信息(不包含功能权限)生成token并存入redis中,然后返回给前端;b.将功能权限也存入redis中
3.用户下次访问请求根据@Auth注解决定是否携带token请求头,服务器拿到该token值先去校验是否有效,然后拼接key,根据key获取redis中token值(authInfo),将authInfo通过json序列化转换为AuthInfo对象,然后根据请求参数对应的类是否包含了该AuthInfo属性,包含了就将authInfo设置到属性上,不包含就设置到ThreadLocal中,反正请求中都会拥有用户信息,方便数据库操作记录用户信息。访问了之后又给token续xx时间