用户权限管理常通过拦截器拦截指定url实现,本项目中使用restful风格,可根据url拦截,但仍然不太方便。项目中的实现是,自定义一个注解,将它用在需要登录/某种权限的方法中,然后在拦截器中判断要访问的方法是否有我们自定义的注解
,如果有就判断当前用户是否登录了(判断是否携带了登录之后获取到的token),从而决定是否拦截。
UserRole.java
项目使用枚举类定义用户角色权限,value值越小权限越高,高权限具有低权限所有功能
public enum UserRole {
// 值越小权限越高,高权限具有低权限所有功能
ADMIN(0), // 超级管理员
xxx(10),
MANAGER(20),
xxx(30),
EVERYONE(1000); // 表示所有用户,用来接口认证
@Getter
private int value;
UserRole(int value) {
this.value = value;
}
}
自定义一个注解类,在需要控制权限的方法前注解
UserAuthentication.java
, 用来标识该接口需要进行权限控制,并且value默认为 UserRole.EVERYONE
及所有人。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAuthentication {
UserRole value() default UserRole.EVERYONE;
}
ElementType.MeTHOD
表示该自定义注解可以用在方法上
RetentionPolicy.RUNTIME
表示该注解在代码运行时起作用
在方法上使用注解,例如:
@PostMapping("/login")
@UserAuthentication // 不加为默认值 EVERYONE
public void doSomething() {
}
@UserAuthentication(UserRole.MANAGER) //可指定该接口访问级别,表示只有管理员可以访问
AuthorizationInterceptor.java
继承自 HandlerInterceptorAdapter
HandlerInterceptorAdapter
有三个方法
1). public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
return true;
}
2). public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {
}
3). public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {
}
preHandle在业务处理器处理请求之前被调用。预处理,可以进行编码、安全控制等处理;
postHandle在业务处理器处理请求执行完成后,生成视图之前执行。后处理(调用了Service并返回ModelAndView,但未进行页面渲染),有机会修改ModelAndView;
afterCompletion在DispatcherServlet完全处理完请求后被调用,可用于清理资源等。返回处理(已经渲染了页面),可以根据ex是否为null判断是否发生了异常,进行日志记录;
本项目为前后端分离项目,无需进行曲页面渲染,项目通过重写 preHandle()
方法来进行鉴权,返回值为 false
则不放行该请求,若为true
则放行。
preHandle 主要判断用户角色权限是否符合,以及是否操作的是当前租户资源,起到隔离的效果。
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {
public static final String TOKEN_HEADER = "Authorization";
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ParseException {
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 判断接口是否需要登录/指定权限
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//判断是否有 @UserAuthentication注解
UserAuthentication authAnnotation = method.getAnnotation(UserAuthentication.class);
//若无 直接放行
if (authAnnotation == null) {
return true;
}
// 若有 执行认证
String token = request.getHeader(TOKEN_HEADER);
if (token == null && request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if (cookie.getName().equals(TOKEN_HEADER)) {
token = cookie.getValue();
break;
}
}
}
if (token != null && !token.isEmpty()) {
// 通过token获取用户信息
UserSession session = userService.getUserSession(token);
if (session != null) {
// 当前登录用户的权限 <= 指定权限则可使用该接口
if (session.getRole() != null && session.getRole().getValue() <= authAnnotation.value().getValue()) {
Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
// 获取resful url中的tenantId值
String userIdStr = (String) pathVariables.get("userId");
if (userIdStr != null) {
Long userIdId = Long.valueOf((String) pathVariables.get("userId"));
// 两个判断:(1)用户角色判断 (2)判断当前用户要操作的资源是否是自己userId下的所属资源
if (session.getRole().getValue() <= UserRole.ADMIN.getValue() || session.getTenantId().equals(userId)) {
request.setAttribute(“USER”, session); // 如果是将查询到的用户session放入request中,后续参数解析器解析,方便使用
return true;
}
} else {
request.setAttribute(“USER“, session);
return true;
}
}
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 权限不符
return false;
}
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //未授权
return false;
}
(4)自定义参数解析类
编写 UserSessionArgumentResolver.java
继承自 HandlerMethodArgumentResolver
HandlerMethodArgumentResolver = HandlerMethod + Argument(参数) + Resolver(解析器), 其实就是HandlerMethod方法的解析器, 将 HttpServletRequest(header + body 中的内容)解析为HandlerMethod方法的参数。
这里的主要作用是将preHandle()
那里设置的UserSession
对象解析为controller的的形参,方便使用。
public class UserSessionArgumentResolver implements HandlerMethodArgumentResolver {
// 用于判断是否需要处理该参数分解,返回true表示需要,并且会调用下面的resolveArgument方法
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(UserSession.class);
}
/**
* @return Object 返回controller方法上的形参对象,这里返回的是UserSession.class对象。
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
return webRequest.getAttribute(“USER”, RequestAttributes.SCOPE_REQUEST);
}
}
(5)配置拦截器及注册参数解析器
springboot 配置很方便,编写AuthorizationConfig.java
类, 继承WebMvcConfigurerAdapter
类。
@Configuration
public class AuthorizationConfig implements WebMvcConfigurer {
@Autowired
private AuthorizationInterceptor authorizationInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authorizationInterceptor); //拦截所有请求
}
@Override // 自定义参数解析
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new UserSessionArgumentResolver());
}
}
(6) 使用
拿退出方法来说,方法上使用注解,value值为空指默认,即所有人都可以使用此接口,此时session对象就是由参数解析器解析得到的,并不是前端直接传过来的
@PostMapping("/logout")
@UserAuthentication
public ResponseEntity<BaseResponse> logout(UserSession session, HttpServletResponse response) {
userService.logout(session);
return ResponseEntity.ok(BaseResponse.ok());
}
要加其他权限更改注解的value值即可
@PostMapping("/login")
@Transactional
@UserAuthentication(UserRole.MANAGER)
public void doSomething() {
}
项目权限部分的实现大致就是这样。
参考: