使用拦截器(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 {
// 模拟用户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 {
}
由于使用了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之前,我们先要定义一个注解,这个注解可以加在方法上,指定某个方法需要什么角色,如果不会注解的可以看廖雪峰的官方网站--定义注解。
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、未登录用户访问页面
可以看到,我们自定义的拦截器,成功拦截了未登录用户的访问。
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是无法正常访问业务的。
以上,我们就完成了使用拦截器实现身份校验、权限控制和日志记录的全部功能。