基于拦截器Interceptor实现简易权限控制及行为记录功能

一、业务需求

        使用拦截器(Interceptor),实现Controller中方法的权限控制,并记录访问行为。要求仅在Controller方法上加注解,就可以实现权限控制。具体为:

        1、拦截未登录用户的访问;

        2、拦截不具有权限用户的访问;

        3、用户访问成功,记录访问时间、设备等信息。

基于拦截器Interceptor实现简易权限控制及行为记录功能_第1张图片

对拦截器还不了解的可以看我这一篇文章《Java拦截器(Interceptor)和过滤器(Filter)实例详解》

二、数据库设计 

        简单设计两个数据库,一个是用户表,一个是日志表。

用户表建表语句:

CREATE TABLE `user` (
  `user_id` varchar(255) NOT NULL,
  `user_role` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

存入两个模拟数据:

日志表建表语句:

CREATE TABLE `record` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `ip` varchar(255) DEFAULT NULL,
  `method` varchar(255) DEFAULT NULL,
  `browser` varchar(255) DEFAULT NULL,
  `time` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

里面数据差不多这个样子:

基于拦截器Interceptor实现简易权限控制及行为记录功能_第2张图片

        接着是实体类,及其对应的Mapper,这里使用了lombok和Mybatis plus。

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Hao
 * @program: DockerTest
 * @description: 用户
 * @date 2023-10-23 15:22:36
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User {

    // 模拟用户ID
    private String userId;
    // 模拟用户角色
    private String userRole;
}
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Hao
 * @program: DockerTest
 * @description: 访问记录
 * @date 2023-10-20 12:09:27
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("record")
public class Record {

    // 自增ID
    @TableId(type = IdType.AUTO)
    private Long id;

    // 访问IP
    private String ip;

    // 请求方式
    private String method;

    // 浏览器标识
    private String browser;

    // 访问时间
    private String time;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hao.dockertest.po.User;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author Hao
 * @program: DockerTest
 * @description: User Mapper
 * @date 2023-10-23 15:24:49
 */
@Mapper
public interface UserDAO extends BaseMapper {
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hao.dockertest.po.Record;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author Hao
 * @program: DockerTest
 * @description: Record Mapper
 * @date 2023-10-20 12:12:03
 */
@Mapper
public interface RecordDAO extends BaseMapper {
}

三、Service代码

        由于使用了Mybatis Plus,这里开发就简单很多

首先是UserService

import com.baomidou.mybatisplus.extension.service.IService;
import com.hao.dockertest.po.User;

/**
 * @author Hao
 * @program: DockerTest
 * @description: User接口
 * @date 2023-10-23 15:24:34
 */
public interface UserService extends IService {

    // 检查用户对应的角色
    boolean checkUserRole(String userId, String userRole);
}

然后是其实现类,只有一个简单的校验用户角色和传入的角色是否相等。

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hao.dockertest.mapper.UserDAO;
import com.hao.dockertest.po.User;
import com.hao.dockertest.service.UserService;
import org.springframework.stereotype.Service;

/**
 * @author Hao
 * @program: DockerTest
 * @description: UserService实现类
 * @date 2023-10-23 15:25:48
 */
@Service
public class UserServiceImpl extends ServiceImpl implements UserService {

    /**
     * 检查用户对应的角色
     * @param userId 用户ID
     * @param userRole 要检查的角色
     * @return yes or no
     */
    @Override
    public boolean checkUserRole(String userId, String userRole) {

        User user = this.lambdaQuery().eq(User::getUserId, userId).one();
        if(user != null)    return userRole.equals(user.getUserRole()); // 返回要检查的用户角色是否和数据库中存储的角色对于
        return false;
    }
}

然后是日志接口和实现类,里面都没有东西,因为Mybatis plus帮我们做了。

import com.baomidou.mybatisplus.extension.service.IService;
import com.hao.dockertest.po.Record;

/**
 * @author Hao
 * @program: DockerTest
 * @description: Record接口层
 * @date 2023-10-20 12:12:42
 */
public interface RecordService extends IService {

}
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hao.dockertest.mapper.RecordDAO;
import com.hao.dockertest.po.Record;
import com.hao.dockertest.service.RecordService;
import org.springframework.stereotype.Service;

/**
 * @author Hao
 * @program: DockerTest
 * @description: RecordService实现类
 * @date 2023-10-20 12:25:56
 */
@Service
public class RecordServiceImpl extends ServiceImpl implements RecordService {

}

接下来再弄两个工具类,一个是JWT工具类,用于生成token、验证token和解析token;还有一个是时间日期格式的(简单写下)。

import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;

import java.util.Date;
import java.util.UUID;

/**
 * @author Hao
 * @program: DockerTest
 * @description: JWT工具
 * @date 2023-10-23 10:44:36
 */
@Slf4j
public class JWTUtil {

    private static long time = 1000*60*60*10;
    // 签名
    private static final String signature = "test";

    // 生产Token
    public static String createToken(String userName, String userID){
        JwtBuilder jwtBuilder = Jwts.builder();//构建JWT对象
        return jwtBuilder
                // Header
                .setHeaderParam("typ","JWT")
                .setHeaderParam("alg","HS256")
                // payload
                .claim("userName",userName)
                .claim("userId", userID)
                // 设置有效期(毫秒单位)
                .setExpiration(new Date(System.currentTimeMillis()+time))
                .setId(UUID.randomUUID().toString())
                // signature
                .signWith(SignatureAlgorithm.HS256, signature)
                // compact拼接三部分header、payload、signature
                .compact();
    }

    // 验证Token
    public static Boolean checkToken(String token){
        if(token == null){
            return false;
        }
        try {
            JwtParser jwtParser = Jwts.parser();
            jwtParser.setSigningKey(signature).parseClaimsJws(token);
            return true;
        }catch (Exception e){
            // log.error("token失效");
            return false;
        }
    }

    // 解析Token
    public static String getTokenInfo(String token, String key){
        if(token == null || key == null)   return null;
        JwtParser parser = Jwts.parser();
        Jws claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);
        Claims payload = claimsJws.getBody();

        // 获取key对于的内容
        return payload.get(key).toString();
    }

}
import java.text.SimpleDateFormat;

/**
 * @author Hao
 * @program: DockerTest
 * @description: 时间格式工具
 * @date 2023-10-22 17:33:19
 */
public class MyTimeUtil {

    public static SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}

 四、Controller代码

        在写Controller之前,我们先要定义一个注解,这个注解可以加在方法上,指定某个方法需要什么角色,如果不会注解的可以看廖雪峰的官方网站--定义注解。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author Hao
 * @program: DockerTest
 * @description: 需要某种角色注解
 * @date 2023-10-23 15:17:18
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {

    // 这个value就存需要什么角色
    String value() default "";
}

        然后就可以定义Controller了,这里我们模拟三个简单方法,分别是:登录,普通用户常规操作,管理员用户查询日志操作。

        1、登录操作,任何人都可以访问,根据用户ID生成token返回给前端,后续前端访问可以携带token访问;

        2、模拟普通用户常规功能:普通用户可以访问的功能;

        3、模拟管理员获取日志表单条记录功能:管理员用户根据日志ID查询某条记录,此操作仅管理员可访问。

import com.hao.dockertest.AOP.RequireRole;
import com.hao.dockertest.po.Record;
import com.hao.dockertest.service.RecordService;
import com.hao.dockertest.util.JWTUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author Hao
 * @program: DockerTest
 * @description: Record管理层
 * @date 2023-10-20 12:13:28
 */
@RestController
@RequestMapping
@Slf4j
public class RecordController {

    @Autowired
    private RecordService recordService;

    /**
     * 模拟用户登录功能(直接返回token,只是模拟)
     * @return token
     */
    @PostMapping("/login")
    public String login(){

        // return JWTUtil.createToken("张三", "20210919"); // 模拟返回登录成功token
        return JWTUtil.createToken("李四", "20210920"); // 模拟返回登录成功token
    }

    /**
     * 模拟普通用户常规功能
     * @return 返回Welcome
     */
    @GetMapping
    @RequireRole("common")
    public String getInfo(){

        return "Welcome!";
    }

    /**
     * 模拟管理员获取日志表单条记录功能
     * @param id 日志ID
     * @return 日志内容
     */
    @GetMapping("/getInfo/{id}")
    @RequireRole("admin")
    public String getRecord(@PathVariable Long id){

        if(id == null)  return "Id must not null!";
        Record record = recordService.getById(id);
        if(record == null)  return "Do not have this record!";

        return record.toString();
    }
}

        至此,我们的基本业务以及模拟完成,下面就需要定义我们的拦截器,实现拦截需求。

五、自定义拦截器

import com.hao.dockertest.AOP.RequireRole;
import com.hao.dockertest.po.Record;
import com.hao.dockertest.service.RecordService;
import com.hao.dockertest.service.UserService;
import com.hao.dockertest.util.JWTUtil;
import com.hao.dockertest.util.MyTimeUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;


/**
 * @author Hao
 * @program: DockerTest
 * @description: 拦截器
 * @date 2023-10-21 21:13:27
 */
@Slf4j
@Configuration
public class MyInterceptor implements HandlerInterceptor {

    @Autowired
    private RecordService recordService;

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("拦截器前置处理 preHandle");

        // 验证token
        String token = request.getHeader("token");
        if(token == null || !JWTUtil.checkToken(token)){
            log.error("未登录或身份信息验证失败");
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=UTF-8");
            PrintWriter printWriter = response.getWriter();
            printWriter.write("您还未登录或身份验证失败,请重新登录!");
            return false;
        }

        // 验证角色
        HandlerMethod method = (HandlerMethod) handler; // 此处仅是模拟,理论上应该先使用instanceof检验
        RequireRole requireRole = method.getMethodAnnotation(RequireRole.class); // 通过反射获取方法注解
        if(requireRole != null){
            String userId = JWTUtil.getTokenInfo(token, "userId");
            // 判断此用户是否有访问权限
            if(!userService.checkUserRole(userId, requireRole.value())) {
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json;charset=UTF-8");
                PrintWriter printWriter = response.getWriter();
                printWriter.write("您无权访问此功能!");
                log.error("用户{}非法访问,已成功拦截!", userId);
                return false;
            }
        }

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("拦截器后置处理 postHandle");

        // 从HttpServletRequest获取相关信息
        String ip = request.getRemoteAddr();
        if (ip.equals("0:0:0:0:0:0:0:1"))   ip = "127.0.0.1";
        String browser = request.getHeader("Sec-Ch-Ua-Platform");
        String httpMethod = request.getMethod();
        String time = MyTimeUtil.sdf.format(System.currentTimeMillis());

        // 日志存档
        Record record = new Record(null, ip, httpMethod, browser, time);
        recordService.save(record); // 访问日志记录

        // 从token中获取相关信息
        String token = request.getHeader("token");
        String userName = JWTUtil.getTokenInfo(token, "userName");
        String userId = JWTUtil.getTokenInfo(token, "userId");

        log.info("访问用户:{}-{},访问IP:{},访问时间:{},请求方式:{},访问设备:{}",userName, userId, ip, time, httpMethod, browser);

        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("拦截器完成后 afterCompletion");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

        在preHandle()方法中我们首先验证用户是否登录和用户token是否有效,如果无效,直接拦截,让用户登录;验证身份之后,我们还需要验证他的角色是否符合访问方法的要求,我们通过反射获取方法上的注解,并获取注解中的value,与我们数据库中存储的角色进行对比,如果符合要求则放行,如果不符合要求,则拦截访问,并提示用户权限不足。

        在postHandle()方法中,我们从HttpServletRequest中,获取到了用户的IP、浏览器类型、请求方式等信息,持久化到我们的数据库中。

        定义完我们自己的拦截器之后,还要将其配置到Spring MVC中才会生效。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Hao
 * @program: DockerTest
 * @description: Interceptor配置
 * @date 2023-10-21 21:26:18
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Autowired
    private MyInterceptor myInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // 拦截所有请求,排除login
    }
}

        在上面的配置中,首先将我们自定义的拦截器addInterceptor,然后addPathPatterns指定了要拦截哪些路径,这里我们设置全部拦截,但是还要通过excludePathPatterns放行/login,不然用户无法登录。

六、功能测试

        这里我们使用postman进行功能测试,分为以下几种情况:

1、未登录用户访问页面

基于拦截器Interceptor实现简易权限控制及行为记录功能_第3张图片

可以看到,我们自定义的拦截器,成功拦截了未登录用户的访问。

2、用户执行登录,返回token

基于拦截器Interceptor实现简易权限控制及行为记录功能_第4张图片

可以看到,用户成功登录,且后端控制台并未打印信息,说明excludePathPatterns中我们放行了/login起到了作用。

3、普通用户20210919访问只需普通角色就可以访问的功能

基于拦截器Interceptor实现简易权限控制及行为记录功能_第5张图片

可以看到,普通用户成功访问了getInfo()方法,并在访问后,我们的日志记录功能,成功记录了此用户的访问(注意我们的数据库并没有记录访问用户的ID,这个可以自行加,不难)。

4、普通用户20210919访问需admin管理员角色才能访问的方法,例如getRecord()

基于拦截器Interceptor实现简易权限控制及行为记录功能_第6张图片

可以看到,此用户是无法访问需要admin角色的功能的,非法访问已经被成功拦截。

5、管理员用户20210920访问需 admin管理员角色才能访问的方法

首先我执行管理员用户的登录方法,获取管理员token(把login()方法中的return换成20210920即可)。

基于拦截器Interceptor实现简易权限控制及行为记录功能_第7张图片

可以看到拥有admin身份的 20210920用户,成功访问到了指定的日志内容。

6、token失效或被篡改之后的拦截效果

我们随便删除token中的几个字符,模拟token失效或者被恶意篡改

基于拦截器Interceptor实现简易权限控制及行为记录功能_第8张图片

可以看到失效的token是无法正常访问业务的。

以上,我们就完成了使用拦截器实现身份校验、权限控制和日志记录的全部功能。

你可能感兴趣的:(SpringBoot,java,spring,servlet,mybatis)