Springboot基于拦截器的鉴权实现

文章介绍了如何利用拦截器和自定义注解实现简单的角色鉴权功能。通过创建`RequireAuth`、`RequirePermission`和`PermitAll`三个注解,结合拦截器的`preHandle`方法,实现对API的鉴权控制。拦截器通过反射获取注解信息,进行登录和权限检查,未通过时抛出异常并由全局异常处理器处理。最后将拦截器配置到Spring MVC中,完成鉴权功能。

文章目录

前言

用户鉴权是后端服务常见的需求,常见的做法是在用户访问API前根据用户当前的角色验证用户是否有访问权限。在大型项目中一般会使用Spring Security或Shiro等成熟的安全框架,而在一些小项目中,我们可以利用拦截器+自定义注解来完成简单的角色鉴权功能。

说到自定义注解,大家第一个想到的也许是Spring AOP,利用AOP也能完成鉴权功能,这里选择拦截器是因为它比AOP更适合完成本文需求,通过拦截器preHandle方法暴露的handler参数,我们可以利用反射来获取注解。

鉴权流程

在实现一项功能,特别是复杂的功能前,我习惯画思维导图来明确功能的运作流程。

思维导图

创建鉴权注解,给需要鉴权的API方法/类加上注解。

当用户访问API时,请求进入拦截器,通过反射获取API方法/类上的注解,进行鉴权,鉴权通过则放行,不通过则抛出异常,由全局异常处理器捕获并响应相应的HTTP状态码。

Code

注解

/**
* 鉴权注解,验证用户是否登录
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequireAuth {
}
/**
* 鉴权注解,验证用户是否拥有所需权限
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequirePermission {
/**
* 需要的权限名
*/
String[] value();
/**
* 权限关系,默认为or
*/
Relation relation() default Relation.OR;
enum Relation {
OR, AND
}
}
/**
* 鉴权注解,直接放行
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PermitAll {
}

这里创建了三个注解, 对应三种处理方式,在Controller中进行注解。

@RequireAuth
@RestController
@AllArgsConstructor
public class TestController {
private TestService testService;
// 方法上没有鉴权注解,从类上获取到RequireAuth,进行登录检查
@PostMapping("/api1")
public R<?> api1() {
}
// 方法上获取到PermitAll注解,优先处理,直接放行
@PermitAll
@PostMapping("/api2")
public R<?> api2() {
}
// 谁会同时放两个鉴权注解啊喂!
@RequirePermission
@RequireAuth
@PostMapping("/api3")
public R<?> api3() {
}
}

拦截器

接下来配置拦截器,拦截器是鉴权的主要逻辑实现。

public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull Object handler) {
if (handler instanceof HandlerMethod method) {
Class<?> clazz = method.getBeanType();
// 获取方法上的注解
RequireAuth requireAuth = method.getMethodAnnotation(RequireAuth.class);
RequirePermission requirePermission = method.getMethodAnnotation(RequirePermission.class);
PermitAll permitAll = method.getMethodAnnotation(PermitAll.class);
// 若方法上没有注解,尝试获取类上的注解
if (requireAuth == null && requirePermission == null && permitAll == null) {
requireAuth = clazz.getAnnotation(RequireAuth.class);
requirePermission = clazz.getAnnotation(RequirePermission.class);
permitAll = clazz.getAnnotation(PermitAll.class);
}
// 处理权限鉴定
if (requirePermission != null && !checkPermission(request.getSession(), requirePermission)) {
throw new ForbiddenException("您没有权限进行此操作"); // 抛出自定义异常,需继承RuntimeException
}
// 处理登录鉴定
else if (requireAuth != null && !checkAuth(request.getSession())) {
throw new AuthenticationException("请登录后再操作"); // 抛出自定义异常,需继承RuntimeException
}
// 处理放行
else if (permitAll != null) {
return true;
}
}
return true;
}
/**
* 检查登录
* 省略具体逻辑
*/
private boolean checkAuth(HttpSession session) {
return true;
}
/**
* 检查权限
* Relation为OR则拥有注解中一项权限就通过
* Relation为AND则需拥有注解中所有权限才通过
* 省略具体逻辑
*/
private boolean checkPermission(HttpSession session, RequirePermission anno) {
String[] requirePermissions = anno.value();
RequirePermission.Relation relation = anno.relation();
return true;
}
}

配置

最后把拦截器添加到Spring MVC中。

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 添加鉴权拦截器
registry.addInterceptor(new AuthenticationInterceptor());
}
}

全局异常处理

配置全局异常处理器,处理自定义异常

@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 返回401
* 处理用户未登录的异常
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(AuthenticationException.class)
public R<String> AuthorizationException(Exception ex) {
return R.fail(ex.getMessage());
}
/**
* 返回403
* 处理用户没有权限的异常
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(ForbiddenException.class)
public R<String> ForbiddenException(Exception ex) {
return R.fail(ex.getMessage());
}
}

至此,简单的鉴权功能完成。