一、业务需求
使用拦截器(Interceptor),实现Controller中方法的权限控制,并记录访问行为。要求仅在Controller方法上加注解,就可以实现权限控制。具体为:
1、拦截未登录用户的访问;
2、拦截不具有权限用户的访问;
3、用户访问成功,记录访问时间、设备等信息。
对拦截器还不了解的可以看我这一篇文章《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;
里面数据差不多这个样子:
接着是实体类,及其对应的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 {// 模拟用户IDprivate 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;// 访问IPprivate 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<User> {
}
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<Record> {
}
三、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<User> {// 检查用户对应的角色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<UserDAO, User> implements UserService {/*** 检查用户对应的角色* @param userId 用户ID* @param userRole 要检查的角色* @return yes or no*/@Overridepublic 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<Record> {}
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<RecordDAO, Record> 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";// 生产Tokenpublic 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();}// 验证Tokenpublic 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;}}// 解析Tokenpublic static String getTokenInfo(String token, String key){if(token == null || key == null) return null;JwtParser parser = Jwts.parser();Jws<Claims> 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 {@Autowiredprivate RecordService recordService;/*** 模拟用户登录功能(直接返回token,只是模拟)* @return token*/@PostMapping("/login")public String login(){// return JWTUtil.createToken("张三", "20210919"); // 模拟返回登录成功tokenreturn 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 {@Autowiredprivate RecordService recordService;@Autowiredprivate UserService userService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("拦截器前置处理 preHandle");// 验证tokenString 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);}@Overridepublic 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);}@Overridepublic 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 {@Autowiredprivate MyInterceptor myInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); // 拦截所有请求,排除login}
}
在上面的配置中,首先将我们自定义的拦截器addInterceptor,然后addPathPatterns指定了要拦截哪些路径,这里我们设置全部拦截,但是还要通过excludePathPatterns放行/login,不然用户无法登录。
六、功能测试
这里我们使用postman进行功能测试,分为以下几种情况:
1、未登录用户访问页面
可以看到,我们自定义的拦截器,成功拦截了未登录用户的访问。
2、用户执行登录,返回token
可以看到,用户成功登录,且后端控制台并未打印信息,说明excludePathPatterns中我们放行了/login起到了作用。
3、普通用户20210919访问只需普通角色就可以访问的功能
可以看到,普通用户成功访问了getInfo()方法,并在访问后,我们的日志记录功能,成功记录了此用户的访问(注意我们的数据库并没有记录访问用户的ID,这个可以自行加,不难)。
4、普通用户20210919访问需admin管理员角色才能访问的方法,例如getRecord()
可以看到,此用户是无法访问需要admin角色的功能的,非法访问已经被成功拦截。
5、管理员用户20210920访问需 admin管理员角色才能访问的方法
首先我执行管理员用户的登录方法,获取管理员token(把login()方法中的return换成20210920即可)。
可以看到拥有admin身份的 20210920用户,成功访问到了指定的日志内容。
6、token失效或被篡改之后的拦截效果
我们随便删除token中的几个字符,模拟token失效或者被恶意篡改
可以看到失效的token是无法正常访问业务的。
以上,我们就完成了使用拦截器实现身份校验、权限控制和日志记录的全部功能。