添加网关统一鉴权功能、微服务注解鉴权开关配置
This commit is contained in:
parent
ee3f03f1f1
commit
d26cd851aa
|
|
@ -46,4 +46,19 @@ public class SecurityConstants
|
||||||
* 角色权限
|
* 角色权限
|
||||||
*/
|
*/
|
||||||
public static final String ROLE_PERMISSION = "role_permission";
|
public static final String ROLE_PERMISSION = "role_permission";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存在redis中的控制器路径与权限字符串对应的hash键
|
||||||
|
*/
|
||||||
|
public static final String PATH_PERMISSION_MAP = "path_permission_map";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存的角色名前缀
|
||||||
|
*/
|
||||||
|
public static final String ROLE_PREFIX = "ROLE_";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 匿名角色(公共权限的角色名字)
|
||||||
|
*/
|
||||||
|
public static final String ROLE_ANON = "ROLE_ANON";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
package com.ruoyi.common.security.aspect;
|
package com.ruoyi.common.security.aspect;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import com.ruoyi.common.security.annotation.RequiresLogin;
|
||||||
|
import com.ruoyi.common.security.annotation.RequiresPermissions;
|
||||||
|
import com.ruoyi.common.security.annotation.RequiresRoles;
|
||||||
|
import com.ruoyi.common.security.auth.AuthUtil;
|
||||||
import org.aspectj.lang.ProceedingJoinPoint;
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
import org.aspectj.lang.annotation.Around;
|
import org.aspectj.lang.annotation.Around;
|
||||||
import org.aspectj.lang.annotation.Aspect;
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
import org.aspectj.lang.annotation.Pointcut;
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
import org.aspectj.lang.reflect.MethodSignature;
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.common.security.annotation.RequiresLogin;
|
|
||||||
import com.ruoyi.common.security.annotation.RequiresPermissions;
|
import java.lang.reflect.Method;
|
||||||
import com.ruoyi.common.security.annotation.RequiresRoles;
|
|
||||||
import com.ruoyi.common.security.auth.AuthUtil;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于 Spring Aop 的注解鉴权
|
* 基于 Spring Aop 的注解鉴权
|
||||||
|
|
@ -19,6 +21,7 @@ import com.ruoyi.common.security.auth.AuthUtil;
|
||||||
*/
|
*/
|
||||||
@Aspect
|
@Aspect
|
||||||
@Component
|
@Component
|
||||||
|
@ConditionalOnProperty(prefix = "security.aspect", name = "enabled", havingValue = "true", matchIfMissing = true)
|
||||||
public class PreAuthorizeAspect
|
public class PreAuthorizeAspect
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.ruoyi.common.security.config;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.constant.SecurityConstants;
|
||||||
|
import com.ruoyi.common.core.utils.SpringUtils;
|
||||||
|
import com.ruoyi.common.redis.service.RedisService;
|
||||||
|
import com.ruoyi.common.security.annotation.RequiresPermissions;
|
||||||
|
import com.ruoyi.common.security.annotation.RequiresRoles;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
|
import org.springframework.web.method.HandlerMethod;
|
||||||
|
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存所有api,方便网关鉴权
|
||||||
|
*/
|
||||||
|
@ConditionalOnProperty(prefix = "security.gateway", name = "enabled", havingValue = "true")
|
||||||
|
public class PathPermissionMappingConfig {
|
||||||
|
@Value("${routePrefix}")
|
||||||
|
private String routePrefix;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public PathPermissionMappingConfig execute() {
|
||||||
|
RedisService redisService = SpringUtils.getBean(RedisService.class);
|
||||||
|
RequestMappingHandlerMapping bean = SpringUtils.getBean("requestMappingHandlerMapping");
|
||||||
|
Map<RequestMappingInfo, HandlerMethod> handlerMethods = bean.getHandlerMethods();
|
||||||
|
/**
|
||||||
|
* 路径->权限字符串映射,例如 /user/list_GET->system:user:list
|
||||||
|
*/
|
||||||
|
Map<String, String> pathPermsMap = new TreeMap<>();
|
||||||
|
|
||||||
|
handlerMethods.forEach((k, v) -> {
|
||||||
|
RequiresRoles requiresRoles = v.getMethodAnnotation(RequiresRoles.class);
|
||||||
|
RequiresPermissions requiresPermissions = v.getMethodAnnotation(RequiresPermissions.class);
|
||||||
|
|
||||||
|
Set<RequestMethod> methods = k.getMethodsCondition().getMethods();
|
||||||
|
Set<String> patternValues = k.getPatternValues();
|
||||||
|
/**
|
||||||
|
* @RequestMapping注解
|
||||||
|
*/
|
||||||
|
if(methods.isEmpty()) {
|
||||||
|
methods = new HashSet<>();
|
||||||
|
methods.addAll(Arrays.asList(RequestMethod.GET, RequestMethod.POST));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(requiresPermissions == null && requiresRoles == null) {
|
||||||
|
addPathPermsMap(SecurityConstants.ROLE_ANON, pathPermsMap, methods, patternValues);
|
||||||
|
}
|
||||||
|
if(requiresPermissions != null) {
|
||||||
|
for (String perms : requiresPermissions.value()) {
|
||||||
|
addPathPermsMap(perms, pathPermsMap, methods, patternValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(requiresRoles != null) {
|
||||||
|
for (String role : requiresRoles.value()) {
|
||||||
|
addPathPermsMap(SecurityConstants.ROLE_PREFIX+ role, pathPermsMap, methods, patternValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
System.out.println("pathPermsMap = " + pathPermsMap);
|
||||||
|
redisService.setCacheMap(SecurityConstants.PATH_PERMISSION_MAP, pathPermsMap);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一个path对应多个perms
|
||||||
|
* @param perms
|
||||||
|
* @param pathPermsMap
|
||||||
|
* @param methods
|
||||||
|
* @param patternValues
|
||||||
|
*/
|
||||||
|
private void addPathPermsMap(String perms, Map<String, String> pathPermsMap, Set<RequestMethod> methods, Set<String> patternValues) {
|
||||||
|
for (RequestMethod method : methods) {
|
||||||
|
for (String patternValue : patternValues) {
|
||||||
|
String key = routePrefix + patternValue + "_" + method.name();
|
||||||
|
pathPermsMap.put(key, perms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String[] arr = new String[] {"a", "b"};
|
||||||
|
Set<String> set = Arrays.stream(arr).collect(Collectors.toSet());
|
||||||
|
System.out.println("set = " + set);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -3,3 +3,4 @@ com.ruoyi.common.security.service.TokenService
|
||||||
com.ruoyi.common.security.aspect.PreAuthorizeAspect
|
com.ruoyi.common.security.aspect.PreAuthorizeAspect
|
||||||
com.ruoyi.common.security.aspect.InnerAuthAspect
|
com.ruoyi.common.security.aspect.InnerAuthAspect
|
||||||
com.ruoyi.common.security.handler.GlobalExceptionHandler
|
com.ruoyi.common.security.handler.GlobalExceptionHandler
|
||||||
|
com.ruoyi.common.security.config.PathPermissionMappingConfig
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,6 @@
|
||||||
package com.ruoyi.gateway.filter;
|
package com.ruoyi.gateway.filter;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
import com.ruoyi.common.core.constant.CacheConstants;
|
import com.ruoyi.common.core.constant.CacheConstants;
|
||||||
import com.ruoyi.common.core.constant.HttpStatus;
|
import com.ruoyi.common.core.constant.HttpStatus;
|
||||||
import com.ruoyi.common.core.constant.SecurityConstants;
|
import com.ruoyi.common.core.constant.SecurityConstants;
|
||||||
|
|
@ -19,8 +11,24 @@ import com.ruoyi.common.core.utils.StringUtils;
|
||||||
import com.ruoyi.common.redis.service.RedisService;
|
import com.ruoyi.common.redis.service.RedisService;
|
||||||
import com.ruoyi.gateway.config.properties.IgnoreWhiteProperties;
|
import com.ruoyi.gateway.config.properties.IgnoreWhiteProperties;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网关鉴权
|
* 网关鉴权
|
||||||
*
|
*
|
||||||
|
|
@ -35,9 +43,13 @@ public class AuthFilter implements GlobalFilter, Ordered
|
||||||
@Autowired
|
@Autowired
|
||||||
private IgnoreWhiteProperties ignoreWhite;
|
private IgnoreWhiteProperties ignoreWhite;
|
||||||
|
|
||||||
@Autowired
|
@Resource
|
||||||
private RedisService redisService;
|
private RedisService redisService;
|
||||||
|
|
||||||
|
@Value("${security.gateway.enabled:false}")
|
||||||
|
private boolean gatewayAuth;
|
||||||
|
|
||||||
|
private AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
|
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
|
||||||
|
|
@ -73,16 +85,79 @@ public class AuthFilter implements GlobalFilter, Ordered
|
||||||
{
|
{
|
||||||
return unauthorizedResponse(exchange, "令牌验证失败");
|
return unauthorizedResponse(exchange, "令牌验证失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置用户信息到请求
|
// 设置用户信息到请求
|
||||||
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
|
addHeader(mutate, SecurityConstants.USER_KEY, userkey);
|
||||||
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
|
addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
|
||||||
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
|
addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
|
||||||
// 内部请求来源参数清除
|
// 内部请求来源参数清除
|
||||||
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
|
removeHeader(mutate, SecurityConstants.FROM_SOURCE);
|
||||||
|
// 通过网关鉴权
|
||||||
|
if(gatewayAuth) {
|
||||||
|
// admin不需要鉴权
|
||||||
|
if(isAdmin(userid)) {
|
||||||
|
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
||||||
|
}
|
||||||
|
// 网关验证权限
|
||||||
|
String api = url + "_" + request.getMethod().name();
|
||||||
|
if(!hasPermission(api, userkey)) {
|
||||||
|
log.warn("无权访问:{}", api);
|
||||||
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "无权访问", HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
return chain.filter(exchange.mutate().request(mutate.build()).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isAdmin(String userid) {
|
||||||
|
return "1".equals(userid);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasPermission(String api, String token) {
|
||||||
|
// 使用JSONObject接收,避免导入依赖
|
||||||
|
JSONObject loginUser = redisService.getCacheObject(CacheConstants.LOGIN_TOKEN_KEY + token);
|
||||||
|
// 获取登录用户的资源列表
|
||||||
|
Set<String> permissions = (Set<String>) loginUser.get("permissions");
|
||||||
|
// 获取登录用的角色列表
|
||||||
|
Set<String> roles = (Set<String>) loginUser.get("roles");
|
||||||
|
// 获取系统所有控制器路径与权限对应的map
|
||||||
|
Map<String, String> pathPermsMap = redisService.getCacheMap(SecurityConstants.PATH_PERMISSION_MAP);
|
||||||
|
|
||||||
|
Set<String> matchedPerms = pathPermsMap.entrySet().stream()
|
||||||
|
.filter(entry -> match(entry.getKey(), api))
|
||||||
|
.map(entry -> entry.getValue())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
if(!matchedPerms.isEmpty()) {
|
||||||
|
// 所有角色权限
|
||||||
|
Set<String> rolePerms = matchedPerms.stream().filter(item -> item.startsWith("ROLE_")).collect(Collectors.toSet());
|
||||||
|
// 所有资源权限
|
||||||
|
matchedPerms.removeAll(rolePerms);
|
||||||
|
|
||||||
|
if(!rolePerms.isEmpty()) {
|
||||||
|
if(rolePerms.contains(SecurityConstants.ROLE_ANON)) {
|
||||||
|
log.info("允许访问公共权限:{},{}", api, rolePerms);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
rolePerms = rolePerms.stream().map(item -> item.substring(SecurityConstants.ROLE_PREFIX.length())).collect(Collectors.toSet());
|
||||||
|
// 求交集
|
||||||
|
rolePerms.retainAll(roles);
|
||||||
|
if(!rolePerms.isEmpty()) {
|
||||||
|
log.info("允许访问角色权限:{}, {}", api, rolePerms);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 求交集
|
||||||
|
matchedPerms.retainAll(permissions);
|
||||||
|
if(!matchedPerms.isEmpty()) {
|
||||||
|
log.info("允许访问资源权限:{},{}", api, matchedPerms);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("没有找到匹配的权限:{}, {}", api, matchedPerms);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean match(String pattern, String api) {
|
||||||
|
return antPathMatcher.match(pattern, api);
|
||||||
|
}
|
||||||
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
|
private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
|
|
@ -101,7 +176,7 @@ public class AuthFilter implements GlobalFilter, Ordered
|
||||||
|
|
||||||
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg)
|
private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg)
|
||||||
{
|
{
|
||||||
log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
|
log.error("[鉴权异常处理]请求路径:{}, {}", exchange.getRequest().getPath(), msg);
|
||||||
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
|
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue