使用步骤:
(1)定义注解:先创建自定义注解AnnotationDemo.java文件,类型为@interface,
文件头加@Retention(RetentionPolicy.RUNTIME)和 @Target注解, 其中,
①如果是作用于方法,申明为@Target(ElementType.METHOD);
②如果是作用于类,申明为@Target(ElementType.TYPE);
③如果即可以作用于类也可以作用于方法,申明为
@Target(ElementType.METHOD,ElementType.TYPE);
再按需定义AnnotationDemo.java的类型,其支持的类型有:
普通对象、数组(不支持集合)、枚举、字符串等
(2)业务接口添加注解:根据@Target给其他方法或类加上@AnnotationDemo注解,注解的
参数即为AnnotationDemo.java的类型,如果类型加了default默认值,则可以不赋值,其他类型
都需要赋值。
(3)拦截注解:常用来进行日志记录、权限校验。
以下为几个demo:
一、日志记录:拦截器记录用户访问模块:
1、例1:方法注解
(1)注解定义:
package com.demo.interceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 声明注解作用于方法
@Target(ElementType.METHOD)
// 声明注解运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface LogModule {
String moduleCode();
}
(2)拦截器定义:
package com.demo.interceptor;
import com.demo.enums.LogModuleEnums;
import com.demo.service.LogModuleService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class LogModuleInterceptor implements HandlerInterceptor{
private LogModuleService logModuleService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//方法有无加注解
LogModule checkVer = method.getAnnotation(LogModule.class);
//1、没有加注解的接口直接放行
if( checkVer == null ){
return true;
}
// 2、加了注解,获取模块编码并记录
String moduleCode = checkVer.moduleCode();
BeanFactory factory = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
logModuleService = (LogModuleService) factory.getBean("logModuleService");
logModuleService.insertLogModuleRecord(moduleCode);
}
return true;
}
}
配置
package com.demo.config;
import com.demo.interceptor.LogModuleInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogModuleInterceptor());
}
}
(3)服务:
package com.demo.service.impl;
import com.demo.service.LogModuleService;
import org.springframework.stereotype.Service;
@Service("logModuleService")
public class LogModuleServiceImpl implements LogModuleService {
@Override
public void insertLogModuleRecord(String moduleCode) {
System.out.println("访问模块"+moduleCode);
}
}
(4)接口拦截注解:
package com.demo.controller;
import com.demo.dto.RoleDTO;
import com.demo.enums.LogModuleEnums;
import com.demo.interceptor.LogModule;
import com.demo.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/role")
@RestController
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/listAllRoles")
@LogModule(moduleCode = "role")
public List<RoleDTO> listAllRoles(){
return roleService.listAllRoles();
}
@RequestMapping("/getRoleByRoleCode")
@LogModule(moduleCode = "role")
public RoleDTO getRoleByRoleCode(String roleCode){
return roleService.getRoleByRoleCode(roleCode);
}
}
package com.demo.controller;
import com.demo.dto.UserDTO;
import com.demo.interceptor.LogModule;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/listAllUsers")
@LogModule(moduleCode = "user")
public List<UserDTO> listAllUsers(){
return userService.listAllUsers();
}
@RequestMapping("/getUserByUserAccount")
@LogModule(moduleCode = "user")
public UserDTO getUserByUserAccount(String userAccount){
return userService.getUserByUserAccount(userAccount);
}
}
依次访问接口,后台打印:
2、例2:同时作用于类和方法注解:
(1)定义注解:
package com.demo.interceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 声明注解作用于类、方法
@Target({ElementType.TYPE,ElementType.METHOD})
// 声明注解运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface LogModule {
String moduleCode();
}
(2)拦截器:
package com.demo.interceptor;
import com.demo.enums.LogModuleEnums;
import com.demo.service.LogModuleService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class LogModuleInterceptor implements HandlerInterceptor{
private LogModuleService logModuleService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//方法有无加注解
LogModule annotation = method.getAnnotation(LogModule.class);
//1、没有加注解,判断所在类有没有加注解
if( annotation == null ){
System.out.println(method.getName()+"方法无注解");
annotation = method.getDeclaringClass().getAnnotation(LogModule.class);
if(annotation == null){
System.out.println(method.getName()+"所在类无注解");
return true;
}
System.out.println(method.getName()+"所在类有注解");
}
// 2、加了注解,获取模块编码并记录
String moduleCode = annotation.moduleCode();
BeanFactory factory = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
logModuleService = (LogModuleService) factory.getBean("logModuleService");
logModuleService.insertLogModuleRecord(moduleCode);
}
return true;
}
}
(3)接口注解情况:
package com.demo.controller;
import com.demo.dto.UserDTO;
import com.demo.interceptor.LogModule;
import com.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/listAllUsers")
public List<UserDTO> listAllUsers(){
return userService.listAllUsers();
}
@RequestMapping("/getUserByUserAccount")
@LogModule(moduleCode = "user")
public UserDTO getUserByUserAccount(String userAccount){
return userService.getUserByUserAccount(userAccount);
}
}
package com.demo.controller;
import com.demo.dto.RoleDTO;
import com.demo.enums.LogModuleEnums;
import com.demo.interceptor.LogModule;
import com.demo.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequestMapping("/role")
@RestController
@LogModule(moduleCode = "role")
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/listAllRoles")
public List<RoleDTO> listAllRoles(){
return roleService.listAllRoles();
}
@RequestMapping("/getRoleByRoleCode")
public RoleDTO getRoleByRoleCode(String roleCode){
return roleService.getRoleByRoleCode(roleCode);
}
}
依次访问接口,后台打印:
3、例3:自定义注入枚举,同样同时作用于类和方法:
(1)定义枚举
package com.demo.enums;
public enum LogModuleEnums {
USER("user","用户"),
ROLE("role","角色");
public final String moduleCode;
public final String moduleName;
LogModuleEnums(String moduleCode,String moduleName){
this.moduleCode = moduleCode;
this.moduleName = moduleName;
}
}
(2)定义注解:
package com.demo.interceptor;
import com.demo.enums.LogModuleEnums;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 声明注解作用于类、方法
@Target({ElementType.TYPE,ElementType.METHOD})
// 声明注解运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface LogModule {
LogModuleEnums module();
}
(3)拦截器修改:
LogModuleEnums module = annotation.module();
BeanFactory factory = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
logModuleService = (LogModuleService) factory.getBean("logModuleService");
logModuleService.insertLogModuleRecord(module);
(4)接口拦截:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/listAllUsers")
public List<UserDTO> listAllUsers(){
return userService.listAllUsers();
}
@RequestMapping("/getUserByUserAccount")
@LogModule(module = LogModuleEnums.USER)
public UserDTO getUserByUserAccount(String userAccount){
return userService.getUserByUserAccount(userAccount);
}
}
@RequestMapping("/role")
@RestController
@LogModule(module = LogModuleEnums.ROLE)
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/listAllRoles")
public List<RoleDTO> listAllRoles(){
return roleService.listAllRoles();
}
@RequestMapping("/getRoleByRoleCode")
public RoleDTO getRoleByRoleCode(String roleCode){
return roleService.getRoleByRoleCode(roleCode);
}
}
(5)服务:
package com.demo.service.impl;
import com.demo.enums.LogModuleEnums;
import com.demo.service.LogModuleService;
import org.springframework.stereotype.Service;
@Service("logModuleService")
public class LogModuleServiceImpl implements LogModuleService {
@Override
public void insertLogModuleRecord(LogModuleEnums module) {
System.out.println("访问模块"+module.moduleCode+" "+module.moduleName);
}
}
依次访问接口,打印:
以上使用过滤器实现:
二、token校验:拦截器对必须要传入token的接口做拦截:
对项目目录做调整:
1、定义注解:
package com.demo.interceptor.tokencheck;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 声明注解作用于类、方法
@Target({ElementType.TYPE,ElementType.METHOD})
// 声明注解运行时有效
@Retention(RetentionPolicy.RUNTIME)
public @interface TokenCheck {
}
2、定义拦截器:
package com.demo.interceptor.tokencheck;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@Component
public class TokenCheckInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//方法有无加注解
TokenCheck annotation = method.getAnnotation(TokenCheck.class);
//1、没有加注解,判断所在类有没有加注解
if( annotation == null ){
annotation = method.getDeclaringClass().getAnnotation(TokenCheck.class);
if(annotation == null){
return true;
}
}
// 2、加了注解,校验请求头是否加了token
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
System.out.println("头部未传入token");
return false;
}
}
return true;
}
}
3、配置拦截器:
package com.demo.config;
import com.demo.interceptor.logmodule.LogModuleInterceptor;
import com.demo.interceptor.tokencheck.TokenCheckInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TokenCheckInterceptor());
registry.addInterceptor(new LogModuleInterceptor());
}
}
4、接口加注解:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/listAllUsers")
@TokenCheck
public List<UserDTO> listAllUsers(){
return userService.listAllUsers();
}
@RequestMapping("/getUserByUserAccount")
@LogModule(module = LogModuleEnums.USER)
public UserDTO getUserByUserAccount(String userAccount){
return userService.getUserByUserAccount(userAccount);
}
}
@RequestMapping("/role")
@RestController
@LogModule(module = LogModuleEnums.ROLE)
@TokenCheck
public class RoleController {
@Autowired
private RoleService roleService;
@RequestMapping("/listAllRoles")
public List<RoleDTO> listAllRoles(){
return roleService.listAllRoles();
}
@RequestMapping("/getRoleByRoleCode")
public RoleDTO getRoleByRoleCode(String roleCode){
return roleService.getRoleByRoleCode(roleCode);
}
}
测试:
(1)访问http://localhost:7777/filterDemo/user/listAllUsers 不加token
后台打印:头部未传入token
加上token:
后台打印:token校验通过
(2)访问http://localhost:7777/filterDemo/user/getUserByUserAccount?userAccount=zhangsan
后台打印:无需校验token,不拦截
(3)访问/role下接口,情况同(1)。
三、角色权限校验:判断用户是否有访问功能接口的角色或权限,防止越权:
越权有两种:
1)水平越权:指相同权限用户之间在未经授权的情况下,可以访问到一方的资源。比如:同是一个网站的普通用户A和B,A通过越权操作访问了B的信息。这种越权可以按用户维度去控制数据权限,比如查询中带上用户的区域范围等。
2)垂直越权:指没有做好不同角色之间的权限控制,或仅仅在菜单上做了权限控制,低权限用户实现了高权限用户的功能。比如普通用户通过越权登录到了管理员页面,实现管理员才能的操作。下面用自定义拦截的方式解决垂直越权问题。
1、自定义拦截:从接口中获取两个参数,菜单类型(菜单or按钮)、接口对应的权限编码,菜单类型默认为菜单,因为一个接口可能在多个菜单页面都有调用,所以这里权限编码是数组
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AuthVerify {
/**
* 操作类型,MenuTypeEnums
*/
MenuTypeEnums menuType() default MenuTypeEnums.MENU_TYPE_MENU;
/**
* 权限编码,MenuCodeEnums
*/
MenuCodeEnums[] authMenuCodes();
}
对应的枚举:
public enum MenuTypeEnums {
MENU_TYPE_MENU(1,"菜单"),
MENU_TYPE_ACTION(2,"按钮"),
SYSTEM(3,"系统资源")
;
public final Integer code;
public final String message;
MenuTypeEnums(int code, String message ) {
this.code = code;
this.message = message;
}
}
public enum MenuCodeEnums {
USER_MANAGE("web:userManage","用户管理"),
USER_MANAGE_ADD("web:userManage:add","新增用户")
;
public final String code;
public final String name;
MenuCodeEnums(String code, String name) {
this.code = code;
this.name = name;
}
}
2、业务接口注解:
@PostMapping(value = "/user/getPageList")
@AuthVerify(authMenuCodes = {MenuCodeEnums.USER_MANAGE,MenuCodeEnums.USER_MANAGE_ADD})
public ResponseMessage getPageList(){
//
}
@GetMapping("/listPositionByName")
@AuthVerify(menuType = MenuTypeEnums.SYSTEM,authMenuCodes = {})
public ResponseMessage listPositionByName(@RequestParam(value = "positionName", required = false) String positionName){
//
}
3、拦截判断:
@Component
public class AuthCheckInterceptor implements HandlerInterceptor {
private static Logger logger = LoggerFactory.getLogger(HandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//方法有无加注解
AuthVerify annotation = method.getAnnotation(AuthVerify.class);
if (annotation == null){
return true;
}
//有注解
//1、资源类型是否是系统,是系统均有查看权限
Integer authMenuType = annotation.menuType().code;
if(MenuTypeEnums.SYSTEM.code.equals(authMenuType)){
logger.info("AuthCheckInterceptor.preHandle 接口资源类型无需校验,放行");
return true;
}
//2、资源类型是普通菜单/按钮,控制访问权限
MenuCodeEnums[] authMenuCodeEnums = annotation.authMenuCodes();
JwtUser jwtUser = (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userName = jwtUser.getUsername();
List<MenuVO> userMenus = jwtUser.getMenuVOList();
List<String> userMenuCodes = userMenus.stream().map(MenuVO::getMenuCode).collect(Collectors.toList());
for(MenuCodeEnums menuCodeEnum : authMenuCodeEnums){
String authMenuCode = menuCodeEnum.menuCode;
if(userMenuCodes.contains(authMenuCode)){
return true;
}
}
//处理请求失败返回
logger.warn("AuthCheckInterceptor.preHandle,userName={}无权限访问",userName);
setErrorRes(response);
return false;
}
} catch (Exception e) {
logger.error("AuthCheckInterceptor.preHandle异常", e);
return true;
}
return true;
}
private void setErrorRes(HttpServletResponse response) {
try{
ResponseMessage responseMessage = ResponseMessage.error("没有权限访问");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(JSON.toJSONString(responseMessage));
}catch (Exception e){
logger.error("AuthCheckInterceptor.preHandle设置自定义返回异常",e);
}
}
}
拦截通过则正常返回数据,拦截校验失败返回错误信息:
{"code":400,"message":"没有权限访问","status":"error","success":false}
改进:因只有部分接口需要权限拦截校验,使用拦截器的话大部分接口都是走到无注解这步,可以采用AOP切面,只拦截注解的接口。
<!-- aop和aspect -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
public class AuthCheckAspect {
private static Logger logger = LoggerFactory.getLogger(AuthCheckAspect.class);
@Around("@annotation(com.demo.common.AuthVerify)")
public Object doAuthFilter(ProceedingJoinPoint pjp) throws Throwable {
try{
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method targetMethod = methodSignature.getMethod();
AuthVerify annotation = targetMethod.getAnnotation(AuthVerify.class);
//非AuthVerify权限类注解,放开
if (annotation == null) {
return pjp.proceed();
}
//有注解
//1、资源类型是否是系统,是系统均有查看权限
Integer authMenuType = annotation.menuType().code;
if(MenuTypeEnums.SYSTEM.code.equals(authMenuType)){
logger.info("AuthCheckAspect.doAuthFilter 接口资源类型无需校验,放行");
return pjp.proceed();
}
//2、资源类型是普通菜单/按钮,控制访问权限
MenuCodeEnums[] authMenuCodeEnums = annotation.authMenuCodes();
JwtUser jwtUser = (JwtUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userName = jwtUser.getUsername();
List<MenuVO> userMenus = jwtUser.getMenuVOList();
List<String> userMenuCodes = userMenus.stream().map(MenuVO::getMenuCode).collect(Collectors.toList());
for(MenuCodeEnums menuCodeEnum : authMenuCodeEnums){
String authMenuCode = menuCodeEnum.menuCode;
if(userMenuCodes.contains(authMenuCode)){
return pjp.proceed();
}
}
//请求失败
logger.warn("AuthCheckAspect.doAuthFilter,userName={}无权限访问",userName);
return ResponseMessage.error("没有权限访问");
}catch (Exception e){
return pjp.proceed();
}
}
}