This commit is contained in:
kk
2026-07-07 18:01:21 +08:00
commit b259f94d4b
1088 changed files with 121778 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>blade-core-auth</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<artifactId>blade-core-secure</artifactId>
<name>${project.artifactId}</name>
<version>${project.parent.version}</version>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.core.secure</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-auth</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-cache</artifactId>
</dependency>
<!--Jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<exclusion>
<artifactId>tomcat-jdbc</artifactId>
<groupId>org.apache.tomcat</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- Auto -->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-auto</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.constant.RoleConstant;
import java.lang.annotation.*;
/**
* Admin角色权限注解,默认校验是否具有 Admin 角色
* <p>
* 使用示例:
* <pre>{@code
* // 仅管理员可访问
* @IsAdmin
* public R<Boolean> adminOperation() {
* return R.data(true);
* }
* }</pre>
*
* @author BladeX
* @see PreAuth
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuth(RoleConstant.HAS_ROLE_ADMIN)
public @interface IsAdmin {
}
@@ -0,0 +1,53 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.constant.RoleConstant;
import java.lang.annotation.*;
/**
* Administrator角色权限注解,默认校验是否具有 Administrator 角色
* <p>
* 使用示例:
* <pre>{@code
* // 仅超管可访问
* @IsAdministrator
* public R<Boolean> administratorOperation() {
* return R.data(true);
* }
* }</pre>
*
* @author BladeX
* @see PreAuth
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuth(RoleConstant.HAS_ROLE_ADMINISTRATOR)
public @interface IsAdministrator {
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.annotation;
import org.springblade.core.tool.utils.StringPool;
import java.lang.annotation.*;
/**
* 权限注解,用于检查权限,规定访问权限
* 支持以下几种使用方式:
* <p>
* 1. 单个属性模式:
* <pre>{@code
* @PreAuth(permission = "user:add")
* @PreAuth(role = "admin")
* @PreAuth(menu = "user")
* }</pre>
*
* 2. 组合属性模式:
* <pre>{@code
* @PreAuth(role = "admin", permission = "user:add")
* @PreAuth(menu = "user", permission = "user:list")
* }</pre>
*
* 3. SpEL表达式模式:
* <pre>{@code
* @PreAuth("#userVO.id<10")
* @PreAuth("hasMenu('user')")
* @PreAuth("hasRole('admin')")
* @PreAuth("hasPermission('user:add')")
* @PreAuth("hasPermission(#test) and hasRole('admin')")
* }</pre>
*
* @author BladeX
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuth {
/**
* Spring el表达式
*/
String value() default StringPool.EMPTY;
/**
* 接口权限
*/
String permission() default StringPool.EMPTY;
/**
* 角色权限
*/
String role() default StringPool.EMPTY;
/**
* 菜单权限
*/
String menu() default StringPool.EMPTY;
}
@@ -0,0 +1,104 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
public class AdminAspect {
/**
* 切 方法 和 类上的 @IsAdmin 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.IsAdmin) || " +
"@within(org.springblade.core.secure.annotation.IsAdmin)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解,有则跳过角色校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (AuthUtil.isAdmin() || AuthUtil.isAdministrator()) {
return point.proceed();
}
this.handleLogger();
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 记录日志
*/
private void handleLogger() {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n================ IsAdmin Start ================" +
"\nPreAuth : IsAdmin" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n================ IsAdmin End ================\n";
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
}
@@ -0,0 +1,105 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.WebUtil;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
public class AdministratorAspect {
/**
* 切 方法 和 类上的 @IsAdministrator 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.IsAdministrator) || " +
"@within(org.springblade.core.secure.annotation.IsAdministrator)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解,有则跳过角色校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (AuthUtil.isAdministrator()) {
return point.proceed();
}
this.handleLogger();
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 记录日志
*/
private void handleLogger() {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n============ IsAdministrator Start ============" +
"\nPreAuth : IsAdministrator" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n============ IsAdministrator End ============\n";
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
}
@@ -0,0 +1,218 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import jakarta.annotation.security.PermitAll;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.auth.AuthFun;
import org.springblade.core.secure.exception.SecureException;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.MethodParameter;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* AOP 鉴权
*
* @author Chill
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
public class AuthAspect implements ApplicationContextAware {
/**
* 权限处理函数
*/
private final AuthFun authFun;
/**
* 表达式处理
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
/**
* 切 方法 和 类上的 @PreAuth 注解
*
* @param point 切点
* @return Object
* @throws Throwable 没有权限的异常
*/
@Around(
"@annotation(org.springblade.core.secure.annotation.PreAuth) || " +
"@within(org.springblade.core.secure.annotation.PreAuth)"
)
public Object preAuth(ProceedingJoinPoint point) throws Throwable {
// 检查 @PermitAll 注解,有则跳过权限校验
if (ClassUtil.getAnnotation(point, PermitAll.class) != null) {
return point.proceed();
}
if (handleAuth(point)) {
return point.proceed();
}
this.handleLogger(point);
throw new SecureException(ResultCode.UN_AUTHORIZED);
}
/**
* 处理权限
*
* @param point 切点
*/
private boolean handleAuth(ProceedingJoinPoint point) {
// 读取权限注解,优先方法上,没有则读取类
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
// 处理接口权限属性验证
if (StringUtil.isNotBlank(preAuth.permission()) && !authFun.hasPermission(preAuth.permission())) {
return false;
}
// 处理角色权限属性验证
if (StringUtil.isNotBlank(preAuth.role()) && !authFun.hasRole(preAuth.role())) {
return false;
}
// 处理菜单权限属性验证
if (StringUtil.isNotBlank(preAuth.menu()) && !authFun.hasMenu(preAuth.menu())) {
return false;
}
// 判断表达式
String condition = preAuth.value();
if (StringUtil.isNotBlank(condition)) {
Expression expression = EXPRESSION_PARSER.parseExpression(condition);
// 方法参数值
Object[] args = point.getArgs();
StandardEvaluationContext context = getEvaluationContext(method, args);
return Boolean.TRUE.equals(expression.getValue(context, Boolean.class));
} else {
// 判断用户是否通过鉴权
return AuthUtil.hasAuth();
}
}
/**
* 获取方法上的参数
*
* @param method 方法
* @param args 变量
* @return {SimpleEvaluationContext}
*/
private StandardEvaluationContext getEvaluationContext(Method method, Object[] args) {
// 初始化Sp el表达式上下文,并设置 AuthFun
StandardEvaluationContext context = new StandardEvaluationContext(authFun);
// 设置表达式支持spring bean
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
for (int i = 0; i < args.length; i++) {
// 读取方法参数
MethodParameter methodParam = ClassUtil.getMethodParameter(method, i);
// 设置方法 参数名和值 为sp el变量
context.setVariable(methodParam.getParameterName(), args[i]);
}
return context;
}
/**
* 记录日志
*/
private void handleLogger(ProceedingJoinPoint point) {
HttpServletRequest request = WebUtil.getRequest();
if (request != null && AuthUtil.hasAuth()) {
// 读取权限注解,优先方法上,没有则读取类
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
PreAuth preAuth = ClassUtil.getAnnotation(method, PreAuth.class);
String menu = preAuth.menu();
String role = preAuth.role();
String permission = preAuth.permission();
String value = preAuth.value();
// 获取preAuth注解有值的定义
StringBuilder authMessage = new StringBuilder();
if (StringUtil.isNotBlank(menu)) {
authMessage.append("menu[").append(menu).append("] ");
}
if (StringUtil.isNotBlank(role)) {
authMessage.append("role[").append(role).append("] ");
}
if (StringUtil.isNotBlank(permission)) {
authMessage.append("permission[").append(permission).append("] ");
}
if (StringUtil.isNotBlank(value)) {
authMessage.append("SpEL[").append(value).append("] ");
}
// 记录日志
List<Object> authArgs = new ArrayList<>();
String authLogger = "\n\n================ PreAuth Start ================" +
"\nPreAuth : {}" +
"\nClientId : {}" +
"\nBladeAuth : {}" +
"\nBladeUser : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n================ PreAuth End ================\n";
authArgs.add(authMessage.toString());
authArgs.add(AuthUtil.getClientId(request));
authArgs.add(WebUtil.getHeader(AUTH_HEADER));
authArgs.add(AuthUtil.getUser());
authArgs.add(WebUtil.getRequestURI(request));
authArgs.add(WebUtil.getIP(request));
authArgs.add(WebUtil.getRequestContent(request));
log.warn(authLogger, authArgs.toArray());
}
}
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
@@ -0,0 +1,233 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.auth;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.launch.constant.TokenConstant;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.handler.IPermissionHandler;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.constant.RoleConstant;
import org.springblade.core.tool.utils.*;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* 权限判断
*
* @author Chill
*/
public class AuthFun {
/**
* 权限校验处理器
*/
private static IPermissionHandler permissionHandler;
private static IPermissionHandler getPermissionHandler() {
if (permissionHandler == null) {
permissionHandler = SpringUtil.getBean(IPermissionHandler.class);
}
return permissionHandler;
}
/**
* 判断角色是否具有接口权限
*
* @return {boolean}
*/
public boolean permissionAll() {
return getPermissionHandler().permissionAll();
}
/**
* 判断角色是否具有接口权限
*
* @param permission 权限编号
* @return {boolean}
*/
public boolean hasPermission(String permission) {
return getPermissionHandler().hasPermission(permission);
}
/**
* 判断角色是否具有菜单权限
*
* @param permission 菜单编号
* @return {boolean}
*/
public boolean hasMenu(String permission) {
return getPermissionHandler().hasMenu(permission);
}
/**
* 放行所有请求
*
* @return {boolean}
*/
public boolean permitAll() {
return true;
}
/**
* 只有超管角色才可访问
*
* @return {boolean}
*/
public boolean denyAll() {
return hasRole(RoleConstant.ADMIN);
}
/**
* 是否已授权
*
* @return {boolean}
*/
public boolean hasAuth() {
return AuthUtil.hasAuth();
}
/**
* 是否有时间授权
*
* @param start 开始时间
* @param end 结束时间
* @return {boolean}
*/
public boolean hasTimeAuth(Integer start, Integer end) {
Integer hour = DateUtil.hour();
return hour >= start && hour <= end;
}
/**
* 判断是否有该角色权限
*
* @param role 单角色
* @return {boolean}
*/
public boolean hasRole(String role) {
return hasAnyRole(role);
}
/**
* 判断是否具有所有角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAllRole(String... role) {
for (String r : role) {
if (!hasRole(r)) {
return false;
}
}
return true;
}
/**
* 判断是否有该角色权限
*
* @param role 角色集合
* @return {boolean}
*/
public boolean hasAnyRole(String... role) {
BladeUser user = AuthUtil.getUser();
if (user == null) {
return false;
}
String userRole = user.getRoleName();
if (StringUtil.isBlank(userRole)) {
return false;
}
String[] roles = Func.toStrArray(userRole);
for (String r : role) {
if (CollectionUtil.contains(roles, r)) {
return true;
}
}
return false;
}
/**
* 判断请求是否为加密token
*
* @return {boolean}
*/
public boolean hasCrypto() {
HttpServletRequest request = WebUtil.getRequest();
String auth = Objects.requireNonNull(request).getHeader(TokenConstant.AUTH_HEADER);
return JwtUtil.isCrypto(
StringUtil.isNotBlank(auth) ? auth : request.getParameter(TokenConstant.AUTH_HEADER)
);
}
/**
* 判断令牌是否符合严格模式
*
* @return {boolean}
*/
public boolean hasStrictToken() {
BladeUser currentUser = AuthUtil.getUser();
return AuthUtil.userIncomplete(currentUser);
}
/**
* 判断是否包含安全请求头
*
* @return {boolean}
*/
public boolean hasStrictHeader() {
return !AuthUtil.secureHeaderIncomplete();
}
/**
* 判断是否有该请求头
*
* @param header 请求头
* @return {boolean}
*/
public boolean hasHeader(String header) {
HttpServletRequest request = WebUtil.getRequest();
String value = Objects.requireNonNull(request).getHeader(header);
return StringUtil.isNotBlank(value);
}
/**
* 判断是否有该请求头
*
* @param header 请求头
* @param key 请求值
* @return {boolean}
*/
public boolean hasHeader(String header, String key) {
HttpServletRequest request = WebUtil.getRequest();
String value = Objects.requireNonNull(request).getHeader(header);
return StringUtil.equals(value, key);
}
}
@@ -0,0 +1,56 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.BladeRoleSupplier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
/**
* Jackson Views 角色提供者自动装配
* <p>
* 默认通过 {@link AuthUtil#getUserRole()} 获取当前用户角色名,
* 用户可自定义 {@link BladeRoleSupplier} Bean 来覆盖。
* </p>
*
* @author Chill
*/
@AutoConfiguration
public class BladeViewRoleConfiguration {
/**
* 默认角色名称提供者
* <p>使用 {@link ConditionalOnMissingBean} 允许用户自定义覆盖</p>
*/
@Bean
@ConditionalOnMissingBean
public BladeRoleSupplier roleNameSupplier() {
return AuthUtil::getUserRole;
}
}
@@ -0,0 +1,63 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.nonce.LocalNonceStore;
import org.springblade.core.secure.nonce.NonceStore;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
/**
* NonceStore自动配置
* <p>
* 用于防重放攻击检测的Nonce存储配置
* 默认使用本地缓存实现,外部可自行实现NonceStore接口并注册Bean覆盖
*
* @author Chill
*/
@Slf4j
@Order
@AutoConfiguration(before = RegistryConfiguration.class)
public class NonceStoreConfiguration {
/**
* 默认的NonceStore实现(本地缓存)
* <p>
* 使用ConcurrentHashMap存储nonce,适用于单机环境
* 分布式环境可自行实现NonceStore接口(如Redis实现)并注册Bean覆盖
*
* @return LocalNonceStore
*/
@Bean
@ConditionalOnMissingBean(NonceStore.class)
public NonceStore nonceStore() {
return new LocalNonceStore();
}
}
@@ -0,0 +1,37 @@
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.secure.aspect.AdminAspect;
import org.springblade.core.secure.aspect.AdministratorAspect;
import org.springblade.core.secure.aspect.AuthAspect;
import org.springblade.core.secure.auth.AuthFun;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 权限注解配置类
*
* @author BladeX
*/
@EnableAspectJAutoProxy(proxyTargetClass = true)
@AutoConfiguration
@AllArgsConstructor
public class PreAuthConfiguration {
@Bean
public AuthAspect authAspect() {
return new AuthAspect(new AuthFun());
}
@Bean
public AdminAspect adminAspect() {
return new AdminAspect();
}
@Bean
public AdministratorAspect administratorAspect() {
return new AdministratorAspect();
}
}
@@ -0,0 +1,89 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.launch.server.ServerInfo;
import org.springblade.core.secure.handler.*;
import org.springblade.core.secure.props.KeyProperties;
import org.springblade.core.secure.registry.SecureRegistry;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* secure注册默认配置
*
* @author Chill
*/
@Order
@AutoConfiguration(before = SecureConfiguration.class)
@EnableConfigurationProperties({KeyProperties.class})
@AllArgsConstructor
public class RegistryConfiguration {
@Bean
@ConditionalOnMissingBean(SecureRegistry.class)
public SecureRegistry secureRegistry() {
return new SecureRegistry();
}
@Bean
@ConditionalOnMissingBean(ISecureHandler.class)
public ISecureHandler secureHandler() {
return new BladeSecureHandler();
}
@Bean
@ConditionalOnMissingBean(IPermissionHandler.class)
public IPermissionHandler permissionHandler(JdbcTemplate jdbcTemplate) {
return new BladePermissionHandler(jdbcTemplate);
}
@Bean
@ConditionalOnMissingBean(IApiKeyLogHandler.class)
public IApiKeyLogHandler apiKeyLogHandler(JdbcTemplate jdbcTemplate, BladeProperties bladeProperties, ServerInfo serverInfo) {
return new BladeApiKeyLogHandler(jdbcTemplate, bladeProperties, serverInfo);
}
@Bean
@ConditionalOnMissingBean(IApiKeyHandler.class)
public IApiKeyHandler apiKeyHandler(JdbcTemplate jdbcTemplate, KeyProperties keyProperties, IApiKeyLogHandler apiKeyLogHandler) {
return new BladeApiKeyHandler(jdbcTemplate, keyProperties, apiKeyLogHandler);
}
@Bean
@ConditionalOnMissingBean(UserTokenHandler.class)
public UserTokenHandler userTokenHandler() {
return new BladeUserTokenHandler();
}
}
@@ -0,0 +1,143 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.secure.handler.ISecureHandler;
import org.springblade.core.secure.nonce.NonceStore;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.props.BladeSecureProperties;
import org.springblade.core.secure.props.SignSecure;
import org.springblade.core.secure.registry.SecureRegistry;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.annotation.Order;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
import java.util.stream.Collectors;
/**
* 安全配置类
*
* @author Chill
*/
@Order
@AutoConfiguration
@AllArgsConstructor
@EnableConfigurationProperties({BladeSecureProperties.class})
public class SecureConfiguration implements WebMvcConfigurer {
private final SecureRegistry secureRegistry;
private final BladeProperties bladeProperties;
private final BladeSecureProperties secureProperties;
private final ISecureHandler secureHandler;
private final NonceStore nonceStore;
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
// 设置请求授权
if (secureRegistry.isAuthEnabled() || secureProperties.getAuthEnabled()) {
List<AuthSecure> authSecures = this.secureRegistry.addAuthPatterns(secureProperties.getAuth()).getAuthSecures();
if (!authSecures.isEmpty()) {
registry.addInterceptor(secureHandler.authInterceptor(secureProperties, authSecures));
// 设置路径放行
secureRegistry.excludePathPatterns(authSecures.stream().map(AuthSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置基础认证授权
if (secureRegistry.isBasicEnabled() || secureProperties.getBasicEnabled()) {
List<BasicSecure> basicSecures = this.secureRegistry.addBasicPatterns(secureProperties.getBasic()).getBasicSecures();
if (!basicSecures.isEmpty()) {
registry.addInterceptor(secureHandler.basicInterceptor(basicSecures));
// 设置路径放行
secureRegistry.excludePathPatterns(basicSecures.stream().map(BasicSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置签名认证授权
if (secureRegistry.isSignEnabled() || secureProperties.getSignEnabled()) {
List<SignSecure> signSecures = this.secureRegistry.addSignPatterns(secureProperties.getSign()).getSignSecures();
if (!signSecures.isEmpty()) {
registry.addInterceptor(secureHandler.signInterceptor(signSecures, nonceStore));
// 设置路径放行
secureRegistry.excludePathPatterns(signSecures.stream().map(SignSecure::getPattern).collect(Collectors.toList()));
}
}
// 设置令牌严格模式
if (!secureRegistry.isStrictToken()) {
secureProperties.setStrictToken(false);
}
// 设置请求头严格模式
if (!secureRegistry.isStrictHeader()) {
secureProperties.setStrictHeader(false);
}
// 设置客户端授权
if (secureRegistry.isClientEnabled() || secureProperties.getClientEnabled()) {
secureProperties.getClient().forEach(clientSecure -> {
InterceptorRegistration clientRegistration = registry.addInterceptor(secureHandler.clientInterceptor(clientSecure.getClientId()))
.addPathPatterns(clientSecure.getPathPatterns())
.excludePathPatterns(clientSecure.getExcludePatterns());
configureExcludePatterns(clientRegistration);
});
}
// 设置路径放行
if (secureRegistry.isEnabled() || secureProperties.getEnabled()) {
InterceptorRegistration tokenRegistration = registry.addInterceptor(secureHandler.tokenInterceptor(secureProperties));
configureExcludePatterns(tokenRegistration);
}
}
/**
* 配置拦截器的公共排除路径
*
* @param registration 拦截器注册对象
*/
private void configureExcludePatterns(InterceptorRegistration registration) {
registration
.excludePathPatterns(secureRegistry.getExcludePatterns())
.excludePathPatterns(secureRegistry.getDefaultExcludePatterns())
.excludePathPatterns(secureProperties.getSkipUrl());
// 宽松模式下获取放行路径且再新建一套自定义放行路径,用于处理cloud网关虚拟路径导致未匹配的问题
// 严格模式下不予处理,应严格按照cloud和boot的路由进行匹配
if (!secureProperties.getStrictToken()) {
registration.excludePathPatterns(secureProperties.getSkipUrl().stream()
.map(url -> StringUtil.removePrefix(url, StringPool.SLASH + bladeProperties.getName())).toList());
}
}
}
@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.config;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.resolver.TokenArgumentResolver;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
/**
* Token配置
*
* @author Chill
*/
@Slf4j
@AutoConfiguration
@Order(Ordered.HIGHEST_PRECEDENCE)
@AllArgsConstructor
public class TokenConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new TokenArgumentResolver());
}
}
@@ -0,0 +1,95 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.constant;
/**
* PreAuth权限表达式
*
* @author Chill
*/
public interface AuthConstant {
/**
* 超管别名
*/
String ADMINISTRATOR = "administrator";
/**
* 是有超管角色
*/
String HAS_ROLE_ADMINISTRATOR = "hasRole('" + ADMINISTRATOR + "')";
/**
* 管理员别名
*/
String ADMIN = "admin";
/**
* 是否有管理员角色
*/
String HAS_ROLE_ADMIN = "hasAnyRole('" + ADMINISTRATOR + "', '" + ADMIN + "')";
/**
* 用户别名
*/
String USER = "user";
/**
* 是否有用户角色
*/
String HAS_ROLE_USER = "hasRole('" + USER + "')";
/**
* 测试别名
*/
String TEST = "test";
/**
* 是否有测试角色
*/
String HAS_ROLE_TEST = "hasRole('" + TEST + "')";
/**
* 放行所有请求
*/
String PERMIT_ALL = "permitAll()";
/**
* 只有超管才能访问
*/
String DENY_ALL = "denyAll()";
/**
* 对所有请求进行接口权限校验
*/
String PERMISSION_ALL = "permissionAll()";
/**
* 是否对token加密传输
*/
String HAS_CRYPTO = "hasCrypto()";
}
@@ -0,0 +1,87 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.constant;
import org.springblade.core.tool.utils.StringUtil;
/**
* 权限校验常量
*
* @author Chill
*/
public interface PermissionConstant {
/**
* 获取角色所有的权限编号
*
* @param size 数量
* @return string
*/
static String permissionAllStatement(int size) {
return StringUtil.format("select scope_path as path from blade_scope_api where id in (select scope_id from blade_role_scope where scope_category = 2 and role_id in ({}))", buildHolder(size));
}
/**
* 获取角色指定的权限编号
*
* @param size 数量
* @return string
*/
static String permissionCodeStatement(int size) {
return StringUtil.format("select resource_code as code from blade_scope_api where resource_code = ? and id in (select scope_id from blade_role_scope where scope_category = 2 and role_id in ({}))", buildHolder(size));
}
/**
* 获取角色所有的菜单权限编号
*
* @param size 数量
* @return string
*/
static String permissionMenuStatement(int size) {
return StringUtil.format("select * from blade_menu where is_deleted = 0 and id in (select menu_id from blade_role_menu where role_id in ({}))", buildHolder(size));
}
/**
* 获取角色所有的菜单权限编号
*
* @return string
*/
static String permissionAllMenuStatement() {
return "select * from blade_menu where is_deleted = 0";
}
/**
* 获取Sql占位符
*
* @param size 数量
* @return String
*/
static String buildHolder(int size) {
StringBuilder builder = StringUtil.builder().append("?,".repeat(Math.max(0, size)));
return StringUtil.removeSuffix(builder.toString(), ",");
}
}
@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.constant;
/**
* 授权校验常量
*
* @author Chill
*/
public interface SecureConstant {
/**
* 认证请求头
*/
String BASIC_HEADER_KEY = "Authorization";
/**
* 认证请求头前缀
*/
String BASIC_HEADER_PREFIX = "Basic ";
/**
* 认证请求头前缀
*/
String BASIC_HEADER_PREFIX_EXT = "Basic%20";
/**
* 认证请求头
*/
String BASIC_REALM_HEADER_KEY = "WWW-Authenticate";
/**
* 认证请求值
*/
String BASIC_REALM_HEADER_VALUE = "basic realm=\"no auth\"";
/**
* 授权认证失败
*/
String AUTHORIZATION_FAILED = "授权认证失败";
/**
* 签名认证失败
*/
String SIGN_FAILED = "签名认证失败";
/**
* 用户信息不完整
*/
String USER_INCOMPLETE = "用户信息不完整,签名认证失败";
/**
* 请求头信息不完整
*/
String SECURE_HEADER_INCOMPLETE = "请求头信息不完整,签名认证失败";
/**
* 客户端令牌解析失败
*/
String CLIENT_TOKEN_PARSE_FAILED = "客户端令牌解析失败";
/**
* 客户端令牌不合法
*/
String INVALID_CLIENT_TOKEN = "客户端令牌不合法";
/**
* Authorization未找到
*/
String AUTHORIZATION_NOT_FOUND = "请求头中未找到 [Authorization] 信息";
}
@@ -0,0 +1,285 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.KeyCrypto;
import org.springblade.core.secure.props.KeyProperties;
import org.springblade.core.secure.provider.ApiKeyInfo;
import org.springblade.core.tool.constant.RoleConstant;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.springblade.core.secure.constant.ApiKeyConstant.*;
/**
* API Key 处理器默认实现
*
* @author Chill
*/
@AllArgsConstructor
public class BladeApiKeyHandler implements IApiKeyHandler {
/**
* 路径匹配器
*/
private static final PathMatcher PATH_MATCHER = new AntPathMatcher();
private final JdbcTemplate jdbcTemplate;
private final KeyProperties keyProperties;
private final IApiKeyLogHandler apiKeyLogHandler;
@Override
public BladeUser getUser(String apiKey) {
long startTime = System.currentTimeMillis();
// 检查功能是否启用
if (!keyProperties.getEnabled()) {
return null;
}
// 检查令牌格式是否合法
String parseKey = KeyCrypto.parseKey(apiKey, keyProperties.getCryptoKey());
if (Func.isBlank(parseKey)) {
return null;
}
// 加载用户信息
BladeUser bladeUser = loadUser(apiKey);
if (bladeUser == null) {
return null;
}
// 验证访问路径权限
HttpServletRequest request = WebUtil.getRequest();
if (request != null) {
// api_path 按完整路径配置,补全服务前缀后匹配
String requestPath = keyProperties.getPathPrefix().concat(request.getRequestURI());
if (!validateApiPath(apiKey, requestPath)) {
return null;
}
}
// 异步保存 API Key 调用日志
long apiKeyId = Func.toLong(bladeUser.getDetail().get(DETAIL_API_KEY_ID));
if (apiKeyId > 0L) {
long time = System.currentTimeMillis() - startTime;
apiKeyLogHandler.saveLog(bladeUser, apiKeyId, time, request);
}
return bladeUser;
}
@Override
public void removeCache(String apiKey) {
CacheUtil.evict(API_KEY_CACHE, CACHE_USER_PREFIX, apiKey, Boolean.FALSE);
CacheUtil.evict(API_KEY_CACHE, CACHE_PATH_PREFIX, apiKey, Boolean.FALSE);
}
@Override
public String generateKey() {
return KeyCrypto.generateKey(keyProperties.getCryptoKey());
}
/**
* 第一步:查询 API Key 基本信息
*
* @param apiKey API Key
* @return ApiKeyInfo(仅含 Key 表字段)
*/
private ApiKeyInfo queryApiKeyInfo(String apiKey) {
List<ApiKeyInfo> results = jdbcTemplate.query(API_KEY_SELECT_STATEMENT, new BeanPropertyRowMapper<>(ApiKeyInfo.class), apiKey);
if (results.isEmpty()) {
return null;
}
return results.get(0);
}
/**
* 第二步:查询关联用户信息并填充至 ApiKeyInfo
*
* @param apiKeyInfo API Key 信息(需含 userId
*/
private void populateUserInfo(ApiKeyInfo apiKeyInfo) {
List<ApiKeyInfo> results = jdbcTemplate.query(
API_KEY_USER_SELECT_STATEMENT, new BeanPropertyRowMapper<>(ApiKeyInfo.class), apiKeyInfo.getUserId()
);
if (results.isEmpty()) {
return;
}
ApiKeyInfo userInfo = results.get(0);
apiKeyInfo.setAccount(userInfo.getAccount());
apiKeyInfo.setName(userInfo.getName());
apiKeyInfo.setRealName(userInfo.getRealName());
apiKeyInfo.setDeptId(userInfo.getDeptId());
apiKeyInfo.setPostId(userInfo.getPostId());
apiKeyInfo.setRoleId(userInfo.getRoleId());
}
/**
* 第三步:解析角色别名,若用户为管理员则返回空字符串
*
* @param roleId 角色ID(逗号分隔)
* @return 角色别名(逗号分隔),管理员返回空字符串
*/
private String resolveRoleName(String roleId) {
if (Func.isBlank(roleId) || StringPool.MINUS_ONE.equals(roleId)) {
return StringPool.EMPTY;
}
Long[] roleIds = Func.toLongArray(roleId);
if (roleIds.length == 0) {
return StringPool.EMPTY;
}
// 构建参数化 IN 子句
String placeholders = String.join(StringPool.COMMA, Collections.nCopies(roleIds.length, "?"));
String sql = String.format(API_KEY_ROLE_SELECT_STATEMENT, placeholders);
List<Map<String, Object>> roles = jdbcTemplate.queryForList(sql, (Object[]) roleIds);
if (roles.isEmpty()) {
return StringPool.EMPTY;
}
// 为了系统安全性考虑,管理员角色不返回角色信息,保持低权限角色调用系统给第三方的接口
boolean isAdmin = roles.stream()
.anyMatch(role -> {
String roleAlias = Func.toStr(role.get(ROLE_ALIAS_COLUMN));
return RoleConstant.ADMINISTRATOR.equals(roleAlias) || RoleConstant.ADMIN.equals(roleAlias);
});
if (isAdmin) {
return StringPool.EMPTY;
}
// 拼接角色别名
return roles.stream()
.map(role -> Func.toStr(role.get(ROLE_ALIAS_COLUMN)))
.filter(Func::isNotBlank)
.collect(Collectors.joining(StringPool.COMMA));
}
/**
* 加载用户信息(优先从缓存获取)
*
* @param apiKey API Key
* @return BladeUser
*/
private BladeUser loadUser(String apiKey) {
// 从缓存获取用户信息
BladeUser bladeUser = CacheUtil.get(API_KEY_CACHE, CACHE_USER_PREFIX, apiKey, BladeUser.class, Boolean.FALSE);
if (bladeUser != null) {
// 若用户ID为空,说明是缓存的空对象标记,返回null防止缓存穿透
return bladeUser.getUserId() != null ? bladeUser : null;
}
// 第一步:查询 API Key 基本信息
ApiKeyInfo apiKeyInfo = queryApiKeyInfo(apiKey);
if (apiKeyInfo == null) {
// 若数据库查询依旧为空,则默认空对象以防止缓存穿透
CacheUtil.put(API_KEY_CACHE, CACHE_USER_PREFIX, apiKey, new BladeUser(), Boolean.FALSE);
return null;
}
// 检查状态
if (apiKeyInfo.getStatus() == null || apiKeyInfo.getStatus() != STATUS_ACTIVE) {
return null;
}
// 检查过期时间
Date expireTime = apiKeyInfo.getExpireTime();
if (expireTime != null && expireTime.before(new Date())) {
return null;
}
// 检查用户ID
if (apiKeyInfo.getUserId() == null) {
return null;
}
// 第二步:查询关联用户信息
populateUserInfo(apiKeyInfo);
if (Func.isBlank(apiKeyInfo.getAccount())) {
return null;
}
// 第三步:解析角色别名(含超管安全判定)
String roleName = resolveRoleName(apiKeyInfo.getRoleId());
// 构建 BladeUser
bladeUser = new BladeUser();
bladeUser.setUserId(apiKeyInfo.getUserId());
bladeUser.setTenantId(apiKeyInfo.getTenantId());
bladeUser.setAccount(apiKeyInfo.getAccount());
bladeUser.setUserName(apiKeyInfo.getName());
bladeUser.setNickName(apiKeyInfo.getRealName());
bladeUser.setClientId(apiKey);
bladeUser.setDeptId(Func.toStrWithEmpty(apiKeyInfo.getDeptId(), StringPool.MINUS_ONE));
bladeUser.setPostId(Func.toStrWithEmpty(apiKeyInfo.getPostId(), StringPool.MINUS_ONE));
bladeUser.setRoleId(Func.toStrWithEmpty(apiKeyInfo.getRoleId(), StringPool.MINUS_ONE));
bladeUser.setRoleName(roleName);
// 解析扩展参数
String extParams = apiKeyInfo.getExtParams();
Kv detail = Func.isNotBlank(extParams) ? JsonUtil.parse(extParams, Kv.class) : null;
if (detail == null) {
detail = Kv.create();
}
detail.set(DETAIL_API_KEY_ID, apiKeyInfo.getId());
bladeUser.setDetail(detail);
// 写入缓存
CacheUtil.put(API_KEY_CACHE, CACHE_USER_PREFIX, apiKey, bladeUser, Boolean.FALSE);
return bladeUser;
}
/**
* 验证请求路径是否有访问权限
*
* @param apiKey API Key
* @param requestPath 请求路径
* @return true 有权限,false 无权限
*/
private boolean validateApiPath(String apiKey, String requestPath) {
// 从缓存获取访问权限路径,未命中则查询数据库
String apiPath = CacheUtil.get(API_KEY_CACHE, CACHE_PATH_PREFIX, apiKey, String.class, Boolean.FALSE);
if (apiPath == null) {
ApiKeyInfo apiKeyInfo = queryApiKeyInfo(apiKey);
apiPath = (apiKeyInfo != null) ? apiKeyInfo.getApiPath() : null;
// 若数据库查询依旧为空,则默认全路径权限以防止缓存穿透
CacheUtil.put(API_KEY_CACHE, CACHE_PATH_PREFIX, apiKey, Func.toStrWithEmpty(apiPath, FULL_PATH), Boolean.FALSE);
}
// 如果未配置访问权限,默认允许所有访问
if (Func.isBlank(apiPath) || FULL_PATH.equals(apiPath)) {
return true;
}
// 解析逗号分隔的路径并匹配
String[] paths = apiPath.split(StringPool.COMMA);
for (String path : paths) {
String trimmedPath = path.trim();
if (Func.isNotBlank(trimmedPath) && PATH_MATCHER.match(trimmedPath, requestPath)) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,105 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.launch.props.BladeProperties;
import org.springblade.core.launch.server.ServerInfo;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.support.IdGenerator;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.UrlUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.lang.Nullable;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import static org.springblade.core.secure.constant.ApiKeyConstant.API_KEY_LOG_INSERT;
/**
* API Key 调用日志处理器默认实现
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class BladeApiKeyLogHandler implements IApiKeyLogHandler {
private final JdbcTemplate jdbcTemplate;
private final BladeProperties bladeProperties;
private final ServerInfo serverInfo;
@Override
public void saveLog(BladeUser bladeUser, Long apiKeyId, long time, @Nullable HttpServletRequest request) {
CompletableFuture.runAsync(() -> {
try {
jdbcTemplate.update(API_KEY_LOG_INSERT, collectLogParams(bladeUser, apiKeyId, time, request));
} catch (Exception logException) {
log.error("API Key日志保存失败: {}", logException.getMessage());
}
});
}
/**
* 收集日志参数
*
* @param bladeUser 认证用户
* @param apiKeyId API Key 主键ID
* @param time 认证耗时(ms)
* @param request 当前请求对象
* @return SQL 参数数组
*/
private Object[] collectLogParams(BladeUser bladeUser, Long apiKeyId, long time, @Nullable HttpServletRequest request) {
String tenantId = Func.toStrWithEmpty(bladeUser.getTenantId(), BladeConstant.ADMIN_TENANT_ID);
String createBy = Func.toStrWithEmpty(bladeUser.getAccount(), bladeUser.getUserName());
// 请求信息
String requestUri = StringPool.EMPTY;
String httpMethod = StringPool.EMPTY;
String remoteIp = StringPool.EMPTY;
String userAgent = StringPool.EMPTY;
String params = StringPool.EMPTY;
if (request != null) {
requestUri = UrlUtil.getPath(request.getRequestURI());
httpMethod = request.getMethod();
remoteIp = WebUtil.getIP(request);
userAgent = Func.toStr(request.getHeader(WebUtil.USER_AGENT_HEADER));
params = WebUtil.getRequestContent(request);
}
return new Object[]{
IdGenerator.getId(), tenantId,
bladeProperties.getName(), serverInfo.getIpWithPort(), serverInfo.getHostName(), bladeProperties.getEnv(),
apiKeyId, requestUri, httpMethod, remoteIp, userAgent, params,
String.valueOf(time), createBy, new Date()
};
}
}
@@ -0,0 +1,200 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springblade.core.cache.utils.CacheUtil;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.provider.PermissionMenu;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.*;
import java.util.stream.Collectors;
import static org.springblade.core.cache.constant.CacheConstant.SYS_CACHE;
import static org.springblade.core.secure.constant.PermissionConstant.*;
/**
* 默认授权校验类
*
* @author Chill
*/
@AllArgsConstructor
public class BladePermissionHandler implements IPermissionHandler {
private static final String SCOPE_CACHE_ROLE = "apiScope:role:";
private static final String SCOPE_CACHE_CODE = "apiScope:code:";
private static final String SCOPE_CACHE_MENU = "apiScope:menu:";
private static final String SCOPE_CACHE_ALL_MENU = "apiScope:allMenu:permission";
private final JdbcTemplate jdbcTemplate;
@Override
public boolean permissionAll() {
HttpServletRequest request = WebUtil.getRequest();
BladeUser user = AuthUtil.getUser();
if (request == null || user == null) {
return false;
}
String uri = request.getRequestURI();
List<String> paths = permissionPath(user.getRoleId());
if (paths.isEmpty()) {
return false;
}
return paths.stream().anyMatch(uri::contains);
}
@Override
public boolean hasPermission(String permission) {
HttpServletRequest request = WebUtil.getRequest();
BladeUser user = AuthUtil.getUser();
if (request == null || user == null) {
return false;
}
List<String> codes = permissionCode(permission, user.getRoleId());
return !codes.isEmpty();
}
@Override
public boolean hasMenu(String permission) {
HttpServletRequest request = WebUtil.getRequest();
BladeUser user = AuthUtil.getUser();
if (request == null || user == null) {
return false;
}
if (AuthUtil.isAdministrator()) {
return true;
}
List<String> codes = permissionMenu(permission, user.getRoleId());
return !codes.isEmpty();
}
/**
* 获取接口权限地址
*
* @param roleId 角色id
* @return permissions
*/
private List<String> permissionPath(String roleId) {
List<String> permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_ROLE, roleId, List.class, Boolean.FALSE);
if (permissions == null) {
List<Long> roleIds = Func.toLongList(roleId);
permissions = jdbcTemplate.queryForList(permissionAllStatement(roleIds.size()), String.class, roleIds.toArray());
CacheUtil.put(SYS_CACHE, SCOPE_CACHE_ROLE, roleId, permissions, Boolean.FALSE);
}
return permissions;
}
/**
* 获取接口权限信息
*
* @param permission 权限编号
* @param roleId 角色id
* @return permissions
*/
private List<String> permissionCode(String permission, String roleId) {
List<String> permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_CODE, permission + StringPool.COLON + roleId, List.class, Boolean.FALSE);
if (permissions == null) {
List<Object> args = new ArrayList<>(Collections.singletonList(permission));
List<Long> roleIds = Func.toLongList(roleId);
args.addAll(roleIds);
permissions = jdbcTemplate.queryForList(permissionCodeStatement(roleIds.size()), String.class, args.toArray());
CacheUtil.put(SYS_CACHE, SCOPE_CACHE_CODE, permission + StringPool.COLON + roleId, permissions, Boolean.FALSE);
}
return permissions;
}
/**
* 获取菜单权限信息
*
* @param permission 菜单编号(支持逗号分隔的多个权限)
* @param roleId 角色id
* @return permissions
*/
private List<String> permissionMenu(String permission, String roleId) {
List<String> permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_MENU, permission + StringPool.COLON + roleId, List.class, Boolean.FALSE);
if (permissions == null) {
// 获取所有菜单
List<PermissionMenu> allMenus = permissionAllMenu();
// 获取角色菜单
List<Long> roleIds = Func.toLongList(roleId);
List<PermissionMenu> roleIdMenus = jdbcTemplate.query(permissionMenuStatement(roleIds.size()), new BeanPropertyRowMapper<>(PermissionMenu.class), roleIds.toArray());
// 反向递归角色菜单所有父级
List<PermissionMenu> routes = new LinkedList<>(roleIdMenus);
roleIdMenus.forEach(roleMenu -> recursion(allMenus, routes, roleMenu));
// 支持逗号分隔的多个权限匹配
Set<String> permissionSet = Arrays.stream(permission.split(StringPool.COMMA))
.map(String::trim)
.collect(Collectors.toSet());
// 获取匹配的菜单权限值
permissions = routes.stream()
.map(PermissionMenu::getCode)
.filter(permissionSet::contains)
.collect(Collectors.toList());
// 写入缓存值
CacheUtil.put(SYS_CACHE, SCOPE_CACHE_MENU, permission + StringPool.COLON + roleId, permissions, Boolean.FALSE);
}
return permissions;
}
/**
* 获取所有菜单权限信息
*/
private List<PermissionMenu> permissionAllMenu() {
List<PermissionMenu> permissions = CacheUtil.get(SYS_CACHE, SCOPE_CACHE_ALL_MENU, StringPool.EMPTY, List.class, Boolean.FALSE);
if (permissions == null) {
permissions = jdbcTemplate.query(permissionAllMenuStatement(), new BeanPropertyRowMapper<>(PermissionMenu.class));
CacheUtil.put(SYS_CACHE, SCOPE_CACHE_ALL_MENU, StringPool.EMPTY, permissions, Boolean.FALSE);
}
return permissions;
}
/**
* 递归获取菜单父级
*
* @param allMenus 所有菜单合集
* @param routes 角色分配的菜单合集
* @param roleMenu 当前菜单
*/
private void recursion(List<PermissionMenu> allMenus, List<PermissionMenu> routes, PermissionMenu roleMenu) {
Optional<PermissionMenu> menu = allMenus.stream().filter(x -> Func.equals(x.getId(), roleMenu.getParentId())).findFirst();
if (menu.isPresent() && !routes.contains(menu.get())) {
routes.add(menu.get());
recursion(allMenus, routes, menu.get());
}
}
}
@@ -0,0 +1,71 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
import org.springblade.core.secure.interceptor.*;
import org.springblade.core.secure.nonce.NonceStore;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.props.BladeSecureProperties;
import org.springblade.core.secure.props.SignSecure;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
/**
* Secure处理器
*
* @author Chill
*/
public class BladeSecureHandler implements ISecureHandler {
@Override
public HandlerInterceptor tokenInterceptor(BladeSecureProperties secureProperties) {
return new TokenInterceptor(secureProperties);
}
@Override
public HandlerInterceptor authInterceptor(BladeSecureProperties secureProperties, List<AuthSecure> authSecures) {
return new AuthInterceptor(authSecures);
}
@Override
public HandlerInterceptor basicInterceptor(List<BasicSecure> basicSecures) {
return new BasicInterceptor(basicSecures);
}
@Override
public HandlerInterceptor signInterceptor(List<SignSecure> signSecures, @Nullable NonceStore nonceStore) {
return new SignInterceptor(signSecures, nonceStore);
}
@Override
public HandlerInterceptor clientInterceptor(String clientId) {
return new ClientInterceptor(clientId);
}
}
@@ -0,0 +1,58 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
/**
* 权限校验通用接口
*
* @author Chill
*/
public interface IPermissionHandler {
/**
* 判断角色是否具有接口权限
*
* @return {boolean}
*/
boolean permissionAll();
/**
* 判断角色是否具有接口权限
*
* @param permission 权限编号
* @return {boolean}
*/
boolean hasPermission(String permission);
/**
* 判断角色是否具有菜单权限
*
* @param permission 权限编号
* @return {boolean}
*/
boolean hasMenu(String permission);
}
@@ -0,0 +1,86 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.handler;
import org.springblade.core.secure.nonce.NonceStore;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.props.BladeSecureProperties;
import org.springblade.core.secure.props.SignSecure;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
/**
* secure 拦截器集合
*
* @author Chill
*/
public interface ISecureHandler {
/**
* token拦截器
*
* @param secureProperties 授权配置
* @return tokenInterceptor
*/
HandlerInterceptor tokenInterceptor(BladeSecureProperties secureProperties);
/**
* auth拦截器
*
* @param authSecures 授权集合
* @return HandlerInterceptor
*/
HandlerInterceptor authInterceptor(BladeSecureProperties secureProperties, List<AuthSecure> authSecures);
/**
* basic拦截器
*
* @param basicSecures 基础认证集合
* @return HandlerInterceptor
*/
HandlerInterceptor basicInterceptor(List<BasicSecure> basicSecures);
/**
* sign拦截器
*
* @param signSecures 签名认证集合
* @param nonceStore Nonce存储(用于防重放检测)
* @return HandlerInterceptor
*/
HandlerInterceptor signInterceptor(List<SignSecure> signSecures, @Nullable NonceStore nonceStore);
/**
* client拦截器
*
* @param clientId 客户端id
* @return clientInterceptor
*/
HandlerInterceptor clientInterceptor(String clientId);
}
@@ -0,0 +1,117 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.auth.AuthFun;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.provider.HttpMethod;
import org.springblade.core.secure.provider.ResponseProvider;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import static org.springblade.core.secure.constant.SecureConstant.AUTHORIZATION_FAILED;
/**
* 自定义授权拦截器校验
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
/**
* 表达式处理
*/
private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
private static final EvaluationContext EVALUATION_CONTEXT = new StandardEvaluationContext(new AuthFun());
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
/**
* 授权集合
*/
private final List<AuthSecure> authSecures;
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
boolean check = authSecures.stream().filter(authSecure -> checkAuth(request, authSecure)).findFirst().map(
authSecure -> checkExpression(authSecure.getExpression())
).orElse(Boolean.TRUE);
if (!check) {
ResponseProvider.logAuthFailure(request, response, AUTHORIZATION_FAILED);
return false;
}
return true;
}
/**
* 检测授权
*/
private boolean checkAuth(HttpServletRequest request, AuthSecure authSecure) {
return checkMethod(request, authSecure.getMethod()) && checkPath(request, authSecure.getPattern());
}
/**
* 检测请求方法
*/
private boolean checkMethod(HttpServletRequest request, HttpMethod method) {
return method == HttpMethod.ALL || (
method != null && method == HttpMethod.of(request.getMethod())
);
}
/**
* 检测路径匹配
*/
private boolean checkPath(HttpServletRequest request, String pattern) {
String servletPath = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null && !pathInfo.isEmpty()) {
servletPath = servletPath + pathInfo;
}
return ANT_PATH_MATCHER.match(pattern, servletPath);
}
/**
* 检测表达式
*/
private boolean checkExpression(String expression) {
Boolean result = EXPRESSION_PARSER.parseExpression(expression).getValue(EVALUATION_CONTEXT, Boolean.class);
return result != null ? result : false;
}
}
@@ -0,0 +1,122 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.provider.HttpMethod;
import org.springblade.core.secure.provider.ResponseProvider;
import org.springblade.core.secure.utils.SecureUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.lang.NonNull;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
import static org.springblade.core.secure.constant.SecureConstant.BASIC_REALM_HEADER_KEY;
import static org.springblade.core.secure.constant.SecureConstant.BASIC_REALM_HEADER_VALUE;
/**
* 基础认证拦截器校验
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class BasicInterceptor implements HandlerInterceptor {
/**
* 表达式匹配
*/
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
/**
* 授权集合
*/
private final List<BasicSecure> basicSecures;
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
boolean check = basicSecures.stream().filter(basicSecure -> checkAuth(request, basicSecure)).findFirst().map(
authSecure -> checkBasic(authSecure.getUsername(), authSecure.getPassword())
).orElse(Boolean.TRUE);
if (!check) {
log.warn("授权认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap()));
response.setHeader(BASIC_REALM_HEADER_KEY, BASIC_REALM_HEADER_VALUE);
ResponseProvider.write(response);
return false;
}
return true;
}
/**
* 检测授权
*/
private boolean checkAuth(HttpServletRequest request, BasicSecure basicSecure) {
return checkMethod(request, basicSecure.getMethod()) && checkPath(request, basicSecure.getPattern());
}
/**
* 检测请求方法
*/
private boolean checkMethod(HttpServletRequest request, HttpMethod method) {
return method == HttpMethod.ALL || (
method != null && method == HttpMethod.of(request.getMethod())
);
}
/**
* 检测路径匹配
*/
private boolean checkPath(HttpServletRequest request, String pattern) {
String servletPath = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null && !pathInfo.isEmpty()) {
servletPath = servletPath + pathInfo;
}
return ANT_PATH_MATCHER.match(pattern, servletPath);
}
/**
* 检测表达式
*/
private boolean checkBasic(String username, String password) {
try {
String[] tokens = SecureUtil.extractAndDecodeAuthorization();
return username.equals(tokens[0]) && password.equals(tokens[1]);
} catch (Exception e) {
log.warn("授权认证失败,错误信息:{}", e.getMessage());
return false;
}
}
}
@@ -0,0 +1,98 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.interceptor;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.provider.ResponseProvider;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.secure.utils.SecureUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.ClassUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 客户端校验拦截器
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class ClientInterceptor implements HandlerInterceptor {
private final String clientId;
/**
* 注解跳过认证缓存,每个Method只做一次反射查询
*/
private static final Map<Method, Boolean> SKIP_AUTH_CACHE = new ConcurrentHashMap<>();
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
// 检查 @PermitAll 注解,有则跳过客户端认证
if (handler instanceof HandlerMethod handlerMethod
&& shouldSkipAuth(handlerMethod.getMethod())) {
return true;
}
// 进行客户端认证
BladeUser user = AuthUtil.getUser();
boolean check = (
user != null &&
StringUtil.equals(clientId, SecureUtil.getClientId()) &&
StringUtil.equals(clientId, user.getClientId())
);
if (!check) {
log.warn("客户端认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap()));
ResponseProvider.write(response);
return false;
}
return true;
}
/**
* 判断方法是否需要跳过客户端认证,结果缓存到ConcurrentHashMap
*
* @param method 请求方法
* @return 是否跳过
*/
private boolean shouldSkipAuth(Method method) {
return SKIP_AUTH_CACHE.computeIfAbsent(method, m ->
ClassUtil.getAnnotation(m, PermitAll.class) != null
);
}
}
@@ -0,0 +1,241 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.nonce.NonceStore;
import org.springblade.core.secure.props.SignSecure;
import org.springblade.core.secure.provider.HttpMethod;
import org.springblade.core.secure.provider.ResponseProvider;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.DigestUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import java.time.Duration;
import java.util.Date;
import java.util.List;
/**
* 签名认证拦截器校验
* <p>
* 支持防重放攻击检测:
* 1. 时间戳校验:验证请求时间是否在有效窗口内
* 2. Nonce校验:通过存储nonce防止重复请求
* 3. 签名校验:验证请求签名是否正确
*
* @author Chill
*/
@Slf4j
public class SignInterceptor implements HandlerInterceptor {
/**
* 表达式匹配
*/
private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
/**
* 授权集合
*/
private final List<SignSecure> signSecures;
/**
* Nonce存储(用于防重放检测)
*/
private final NonceStore nonceStore;
/**
* 请求时间
*/
private static final String TIMESTAMP = "timestamp";
/**
* 随机数
*/
private static final String NONCE = "nonce";
/**
* 时间随机数组合加密串
*/
private static final String SIGNATURE = "signature";
/**
* sha1加密方式
*/
private static final String SHA1 = "sha1";
/**
* md5加密方式
*/
private static final String MD5 = "md5";
/**
* 时间戳有效窗口(秒)
*/
private static final int TIMESTAMP_WINDOW = 10;
/**
* nonce过期时间(秒)
*/
private static final int NONCE_EXPIRE = 60;
/**
* 构造函数
*
* @param signSecures 签名配置列表
* @param nonceStore Nonce存储
*/
public SignInterceptor(List<SignSecure> signSecures, @Nullable NonceStore nonceStore) {
this.signSecures = signSecures;
this.nonceStore = nonceStore;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
boolean check = signSecures.stream().filter(signSecure -> checkAuth(request, signSecure)).findFirst().map(
signSecure -> checkSign(signSecure.getCrypto())
).orElse(Boolean.TRUE);
if (!check) {
log.warn("授权认证失败,请求接口:{},请求IP:{},请求参数:{}", request.getRequestURI(), WebUtil.getIP(request), JsonUtil.toJson(request.getParameterMap()));
ResponseProvider.write(response);
return false;
}
return true;
}
/**
* 检测授权
*/
private boolean checkAuth(HttpServletRequest request, SignSecure signSecure) {
return checkMethod(request, signSecure.getMethod()) && checkPath(request, signSecure.getPattern());
}
/**
* 检测请求方法
*/
private boolean checkMethod(HttpServletRequest request, HttpMethod method) {
return method == HttpMethod.ALL || (
method != null && method == HttpMethod.of(request.getMethod())
);
}
/**
* 检测路径匹配
*/
private boolean checkPath(HttpServletRequest request, String pattern) {
String servletPath = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null && !pathInfo.isEmpty()) {
servletPath = servletPath + pathInfo;
}
return ANT_PATH_MATCHER.match(pattern, servletPath);
}
/**
* 检测签名(含防重放检测)
*
* @param crypto 加密方式
* @return 检测结果
*/
private boolean checkSign(String crypto) {
try {
HttpServletRequest request = WebUtil.getRequest();
if (request == null) {
return false;
}
// 获取头部动态签名信息
String timestamp = request.getHeader(TIMESTAMP);
String nonce = request.getHeader(NONCE);
String signature = request.getHeader(SIGNATURE);
// 1. 判断时间戳是否在合法时间段
if (!checkTimestamp(timestamp)) {
log.warn("授权认证失败,认证信息:{}", "请求时间戳非法");
return false;
}
// 2. 防重放检测:验证nonce是否已使用
if (!checkNonce(nonce)) {
log.warn("授权认证失败,认证信息:{}", "请求nonce重复(疑似重放攻击)");
return false;
}
// 3. 加密签名比对
String sign;
if (MD5.equals(crypto)) {
sign = DigestUtil.md5Hex(timestamp + nonce);
} else if (SHA1.equals(crypto)) {
sign = DigestUtil.sha1Hex(timestamp + nonce);
} else {
sign = DigestUtil.sha1Hex(timestamp + nonce);
}
return sign.equalsIgnoreCase(signature);
} catch (Exception e) {
log.warn("授权认证失败,错误信息:{}", e.getMessage());
return false;
}
}
/**
* 检测时间戳是否在有效窗口内
*
* @param timestamp 时间戳
* @return 是否有效
*/
private boolean checkTimestamp(String timestamp) {
if (Func.isBlank(timestamp)) {
return false;
}
long seconds = Duration.between(new Date(Func.toLong(timestamp)).toInstant(), DateUtil.now().toInstant()).getSeconds();
return seconds >= 0 && seconds <= TIMESTAMP_WINDOW;
}
/**
* 检测nonce是否已使用(防重放)
*
* @param nonce 随机数
* @return true-nonce有效(首次使用),false-nonce已使用(重放攻击)
*/
private boolean checkNonce(String nonce) {
if (Func.isBlank(nonce)) {
return false;
}
// 如果没有配置NonceStore,跳过防重放检测
if (nonceStore == null) {
return true;
}
// 尝试存储nonce,如果存储失败说明nonce已存在
return nonceStore.tryStore(nonce, NONCE_EXPIRE);
}
}
@@ -0,0 +1,146 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.interceptor;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.props.BladeSecureProperties;
import org.springblade.core.secure.provider.ResponseProvider;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.ClassUtil;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.springblade.core.secure.constant.SecureConstant.*;
/**
* 签名认证拦截器
*
* @author Chill
*/
@Slf4j
@AllArgsConstructor
public class TokenInterceptor implements HandlerInterceptor {
private final BladeSecureProperties secureProperties;
/**
* 注解跳过认证缓存,每个Method只做一次反射查询
*/
private static final Map<Method, Boolean> SKIP_AUTH_CACHE = new ConcurrentHashMap<>();
/**
* 严格模式默认跳过的URL列表
*/
private final static List<String> DEFAULT_STRICT_SKIP_URL = new ArrayList<>();
static {
// 严格模式默认跳过的URL
// 为了安全考虑目前不支持通过外部配置方式增加严格模式放行的功能
DEFAULT_STRICT_SKIP_URL.add("/menu/routes");
DEFAULT_STRICT_SKIP_URL.add("/menu/buttons");
DEFAULT_STRICT_SKIP_URL.add("/menu/top-menu");
DEFAULT_STRICT_SKIP_URL.add("/user/info");
DEFAULT_STRICT_SKIP_URL.add("/user/register-guest");
DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/routes");
DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/buttons");
DEFAULT_STRICT_SKIP_URL.add("/blade-system/menu/top-menu");
DEFAULT_STRICT_SKIP_URL.add("/blade-system/user/info");
DEFAULT_STRICT_SKIP_URL.add("/blade-system/user/register-guest");
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
// 检查 @PermitAll 注解,有则跳过身份认证
if (handler instanceof HandlerMethod handlerMethod
&& shouldSkipAuth(handlerMethod.getMethod())) {
return true;
}
// 进行用户认证和安全检查
BladeUser currentUser = AuthUtil.getUser();
if (currentUser == null) {
ResponseProvider.logAuthFailure(request, response, SIGN_FAILED);
return false;
}
// 严格模式下检查Token完整性
if (checkStrictToken(request, currentUser)) {
ResponseProvider.logAuthFailure(request, response, USER_INCOMPLETE);
return false;
}
// 严格模式下检查安全请求头
if (checkStrictHeader()) {
ResponseProvider.logAuthFailure(request, response, SECURE_HEADER_INCOMPLETE);
return false;
}
return true;
}
/**
* 严格模式下检查Token完整性,默认跳过的URL不做检查
*
* @param request 请求对象
* @param currentUser 当前用户
* @return 是否不完整(true表示Token不完整,需拦截)
*/
private boolean checkStrictToken(HttpServletRequest request, BladeUser currentUser) {
String requestUrl = request.getRequestURI();
boolean skip = DEFAULT_STRICT_SKIP_URL.stream()
.anyMatch(requestUrl::equals);
return !skip && secureProperties.getStrictToken() && AuthUtil.userIncomplete(currentUser);
}
/**
* 严格模式下检查安全请求头是否完整
*
* @return 是否不完整(true表示请求头缺失,需拦截)
*/
private boolean checkStrictHeader() {
return secureProperties.getStrictHeader() && AuthUtil.secureHeaderIncomplete();
}
/**
* 判断方法是否需要跳过身份认证,结果缓存到ConcurrentHashMap
*
* @param method 请求方法
* @return 是否跳过
*/
private boolean shouldSkipAuth(Method method) {
return SKIP_AUTH_CACHE.computeIfAbsent(method, m ->
ClassUtil.getAnnotation(m, PermitAll.class) != null
);
}
}
@@ -0,0 +1,134 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.nonce;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
/**
* 基于本地缓存的Nonce存储实现
* <p>
* 使用ConcurrentHashMap + ScheduledExecutorService实现
* <p>
*
* @author Chill
*/
@Slf4j
public class LocalNonceStore implements NonceStore {
/**
* 本地缓存,存储nonce及其过期时间
*/
private final ConcurrentHashMap<String, Long> nonceCache = new ConcurrentHashMap<>();
/**
* 定时清理任务执行器
*/
private final ScheduledExecutorService cleanupExecutor;
/**
* 清理间隔(秒)
*/
private static final long CLEANUP_INTERVAL_SECONDS = 60;
public LocalNonceStore() {
// 创建单线程定时执行器用于清理过期nonce
this.cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "nonce-cleanup");
thread.setDaemon(true);
return thread;
});
// 定期清理过期的nonce
this.cleanupExecutor.scheduleAtFixedRate(
this::cleanupExpired,
CLEANUP_INTERVAL_SECONDS,
CLEANUP_INTERVAL_SECONDS,
TimeUnit.SECONDS
);
}
@Override
public boolean tryStore(String nonce, long expireSeconds) {
String key = buildKey(nonce);
long expireTime = System.currentTimeMillis() + (expireSeconds * 1000);
// putIfAbsent 返回null表示key不存在,设置成功
// 返回非null表示key已存在,设置失败(重放攻击)
Long existing = nonceCache.putIfAbsent(key, expireTime);
if (existing == null) {
return true;
}
// 检查是否已过期,如果过期则可以覆盖
if (System.currentTimeMillis() > existing) {
nonceCache.put(key, expireTime);
return true;
}
return false;
}
@Override
public boolean exists(String nonce) {
String key = buildKey(nonce);
Long expireTime = nonceCache.get(key);
if (expireTime == null) {
return false;
}
// 检查是否已过期
if (System.currentTimeMillis() > expireTime) {
nonceCache.remove(key);
return false;
}
return true;
}
/**
* 清理过期的nonce
*/
private void cleanupExpired() {
long now = System.currentTimeMillis();
nonceCache.entrySet().removeIf(entry -> now > entry.getValue());
}
/**
* 构建缓存key
*
* @param nonce 随机数
* @return 完整的缓存key
*/
private String buildKey(String nonce) {
return CACHE_PREFIX + nonce;
}
/**
* 关闭清理执行器
*/
public void shutdown() {
if (cleanupExecutor != null && !cleanupExecutor.isShutdown()) {
cleanupExecutor.shutdown();
}
}
}
@@ -0,0 +1,72 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.nonce;
/**
* Nonce存储接口,用于防重放攻击检测
* <p>
* 默认使用本地缓存实现 {@link LocalNonceStore}
* <p>
* 分布式环境可自行实现此接口(如Redis实现)并注册为Spring Bean覆盖默认实现:
* <pre>
* {@code
* @Bean
* public NonceStore nonceStore(BladeRedis bladeRedis) {
* return new RedisNonceStore(bladeRedis);
* }
* }
* </pre>
*
* @author Chill
*/
public interface NonceStore {
/**
* 缓存前缀
*/
String CACHE_PREFIX = "blade:secure:nonce:";
/**
* 尝试存储nonce值
* <p>
* 如果nonce已存在,返回false(表示重放攻击)
* 如果nonce不存在,存储并返回true
*
* @param nonce 随机数
* @param expireSeconds 过期时间(秒)
* @return true-存储成功(首次请求),false-已存在(重放攻击)
*/
boolean tryStore(String nonce, long expireSeconds);
/**
* 检查nonce是否存在
*
* @param nonce 随机数
* @return true-存在,false-不存在
*/
boolean exists(String nonce);
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.props;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springblade.core.secure.provider.HttpMethod;
/**
* 自定义授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthSecure {
/**
* 请求方法
*/
private HttpMethod method;
/**
* 请求路径
*/
private String pattern;
/**
* 规则表达式
*/
private String expression;
}
@@ -0,0 +1,60 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.props;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springblade.core.secure.provider.HttpMethod;
/**
* 基础授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BasicSecure {
/**
* 请求方法
*/
private HttpMethod method;
/**
* 请求路径
*/
private String pattern;
/**
* 客户端id
*/
private String username;
/**
* 客户端密钥
*/
private String password;
}
@@ -0,0 +1,103 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.props;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
/**
* 客户端校验配置
*
* @author Chill
*/
@Data
@ConfigurationProperties("blade.secure")
public class BladeSecureProperties {
/**
* 开启鉴权规则
*/
private Boolean enabled = false;
/**
* 开启令牌严格模式
*/
private Boolean strictToken = true;
/**
* 开启请求头严格模式
*/
private Boolean strictHeader = true;
/**
* 鉴权放行请求
*/
private final List<String> skipUrl = new ArrayList<>();
/**
* 开启授权规则
*/
private Boolean authEnabled = true;
/**
* 授权配置
*/
private final List<AuthSecure> auth = new ArrayList<>();
/**
* 开启基础认证规则
*/
private Boolean basicEnabled = true;
/**
* 基础认证配置
*/
private final List<BasicSecure> basic = new ArrayList<>();
/**
* 开启签名认证规则
*/
private Boolean signEnabled = true;
/**
* 签名认证配置
*/
private final List<SignSecure> sign = new ArrayList<>();
/**
* 开启客户端规则
*/
private Boolean clientEnabled = true;
/**
* 客户端配置
*/
private final List<ClientSecure> client = new ArrayList<>();
}
@@ -0,0 +1,54 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.props;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 客户端令牌认证信息
*
* @author Chill
*/
@Data
public class ClientSecure {
/**
* 客户端ID
*/
private String clientId;
/**
* 路径匹配
*/
private final List<String> pathPatterns = new ArrayList<>();
/**
* 放行路径
*/
private final List<String> excludePatterns = new ArrayList<>();
}
@@ -0,0 +1,56 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.props;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springblade.core.secure.provider.HttpMethod;
/**
* 签名授权规则
*
* @author Chill
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SignSecure {
/**
* 请求方法
*/
private HttpMethod method;
/**
* 请求路径
*/
private String pattern;
/**
* 加密方式
*/
private String crypto;
}
@@ -0,0 +1,120 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.provider;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* API Key 信息实体
*
* @author Chill
*/
@Data
public class ApiKeyInfo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 租户ID
*/
private String tenantId;
/**
* 用户ID
*/
private Long userId;
/**
* API Key
*/
private String apiKey;
/**
* 访问路径
*/
private String apiPath;
/**
* 过期时间
*/
private Date expireTime;
/**
* 扩展参数
*/
private String extParams;
/**
* 状态
*/
private Integer status;
/**
* 账号
*/
private String account;
/**
* 用户名
*/
private String name;
/**
* 真实姓名
*/
private String realName;
/**
* 部门ID
*/
private String deptId;
/**
* 岗位ID
*/
private String postId;
/**
* 角色ID
*/
private String roleId;
/**
* 角色名称
*/
private String roleName;
}
@@ -0,0 +1,51 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.provider;
/**
* HttpMethod枚举类
*
* @author Chill
*/
public enum HttpMethod {
/**
* 请求方法集合
*/
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, ALL;
/**
* 匹配枚举
*/
public static HttpMethod of(String method) {
try {
return valueOf(method);
} catch (Exception exception) {
return null;
}
}
}
@@ -0,0 +1,135 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.provider;
import lombok.Data;
import org.springblade.core.tool.utils.Func;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
/**
* 权限菜单类
*
* @author Chill
*/
@Data
public class PermissionMenu implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private Long id;
/**
* 菜单父主键
*/
private Long parentId;
/**
* 菜单编号
*/
private String code;
/**
* 菜单名称
*/
private String name;
/**
* 菜单别名
*/
private String alias;
/**
* 请求地址
*/
private String path;
/**
* 菜单资源
*/
private String source;
/**
* 组件资源
*/
private String component;
/**
* 排序
*/
private Integer sort;
/**
* 菜单类型
*/
private Integer category;
/**
* 操作按钮类型
*/
private Integer action;
/**
* 是否打开新页面
*/
private Integer isOpen;
/**
* 备注
*/
private String remark;
/**
* 是否已删除
*/
private Integer isDeleted;
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
PermissionMenu other = (PermissionMenu) obj;
return Func.equals(this.getId(), other.getId());
}
@Override
public int hashCode() {
return Objects.hash(id, parentId, code);
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.provider;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.api.ResultCode;
import org.springblade.core.tool.constant.BladeConstant;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.WebUtil;
import org.springframework.http.MediaType;
import java.io.IOException;
import java.util.Objects;
import static org.springblade.core.launch.constant.TokenConstant.AUTH_HEADER;
/**
* ResponseProvider
*
* @author Chill
*/
@Slf4j
public class ResponseProvider {
public static void logAuthFailure(HttpServletRequest request, HttpServletResponse response, String reason) {
try {
String authLogger = "{}" +
"\n\n================== Auth Start =================" +
"\nBladeAuth : {}" +
"\nRequestURI : {}" +
"\nRequestIP: {}" +
"\nRequestParam: {}" +
"\n================== Auth End =================\n";
log.warn(authLogger, reason, WebUtil.getHeader(AUTH_HEADER), WebUtil.getRequestURI(request),
WebUtil.getIP(request), WebUtil.getRequestContent(request));
} catch (Exception e) {
log.error("日志记录失败", e);
}
ResponseProvider.write(response);
}
public static void write(HttpServletResponse response) {
R result = R.fail(ResultCode.UN_AUTHORIZED);
response.setCharacterEncoding(BladeConstant.UTF_8);
response.addHeader(BladeConstant.CONTENT_TYPE_NAME, MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
try {
response.getWriter().write(Objects.requireNonNull(JsonUtil.toJson(result)));
} catch (IOException ex) {
log.error(ex.getMessage());
}
}
}
@@ -0,0 +1,346 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.registry;
import lombok.Data;
import lombok.Getter;
import org.springblade.core.secure.props.AuthSecure;
import org.springblade.core.secure.props.BasicSecure;
import org.springblade.core.secure.props.SignSecure;
import org.springblade.core.secure.provider.HttpMethod;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 安全框架统一配置
*
* @author Chill
*/
@Data
public class SecureRegistry {
/**
* 是否开启鉴权
*/
private boolean enabled = true;
/**
* 开启令牌严格模式
*/
private boolean strictToken = true;
/**
* 开启请求头严格模式
*/
private boolean strictHeader = true;
/**
* 是否开启授权
*/
private boolean authEnabled = true;
/**
* 是否开启基础认证
*/
private boolean basicEnabled = true;
/**
* 是否开启签名认证
*/
private boolean signEnabled = true;
/**
* 是否开启客户端认证
*/
private boolean clientEnabled = true;
/**
* 默认放行规则
*/
private final List<String> defaultExcludePatterns = new ArrayList<>();
/**
* 自定义放行规则
*/
private final List<String> excludePatterns = new ArrayList<>();
/**
* 自定义授权集合
*/
@Getter
private final List<AuthSecure> authSecures = new ArrayList<>();
/**
* 基础认证集合
*/
@Getter
private final List<BasicSecure> basicSecures = new ArrayList<>();
/**
* 签名认证集合
*/
@Getter
private final List<SignSecure> signSecures = new ArrayList<>();
public SecureRegistry() {
this.defaultExcludePatterns.add("/actuator/health/**");
this.defaultExcludePatterns.add("/v3/api-docs/**");
this.defaultExcludePatterns.add("/swagger-ui/**");
this.defaultExcludePatterns.add("/oauth/**");
this.defaultExcludePatterns.add("/feign/client/**");
this.defaultExcludePatterns.add("/process/resource-view");
this.defaultExcludePatterns.add("/process/diagram-view");
this.defaultExcludePatterns.add("/manager/check-upload");
this.defaultExcludePatterns.add("/tenant/info");
this.defaultExcludePatterns.add("/static/**");
this.defaultExcludePatterns.add("/assets/**");
this.defaultExcludePatterns.add("/error");
this.defaultExcludePatterns.add("/favicon.ico");
}
/**
* 设置单个放行api
*/
public SecureRegistry skipUrl(String pattern) {
return this.excludePathPattern(pattern);
}
/**
* 设置放行api集合
*/
public SecureRegistry skipUrls(String... patterns) {
return this.excludePathPatterns(patterns);
}
/**
* 设置放行api集合
*/
public void skipUrls(List<String> patterns) {
this.excludePathPatterns(patterns);
}
/**
* 设置单个放行api
*/
public SecureRegistry excludePathPattern(String pattern) {
this.excludePatterns.add(pattern);
return this;
}
/**
* 设置放行api集合
*/
public SecureRegistry excludePathPatterns(String... patterns) {
this.excludePatterns.addAll(Arrays.asList(patterns));
return this;
}
/**
* 设置放行api集合
*/
public void excludePathPatterns(List<String> patterns) {
this.excludePatterns.addAll(patterns);
}
/**
* 设置单个自定义授权
*/
public SecureRegistry addAuthPattern(HttpMethod method, String pattern, String expression) {
this.authSecures.add(new AuthSecure(method, pattern, expression));
return this;
}
/**
* 设置自定义授权集合
*/
public SecureRegistry addAuthPatterns(List<AuthSecure> authSecures) {
this.authSecures.addAll(authSecures);
return this;
}
/**
* 设置基础认证
*/
public SecureRegistry addBasicPattern(HttpMethod method, String pattern, String username, String password) {
this.basicSecures.add(new BasicSecure(method, pattern, username, password));
return this;
}
/**
* 设置基础认证集合
*/
public SecureRegistry addBasicPatterns(List<BasicSecure> basicSecures) {
this.basicSecures.addAll(basicSecures);
return this;
}
/**
* 设置签名认证
*/
public SecureRegistry addSignPattern(HttpMethod method, String pattern, String crypto) {
this.signSecures.add(new SignSecure(method, pattern, crypto));
return this;
}
/**
* 设置签名认证集合
*/
public SecureRegistry addSignPatterns(List<SignSecure> signSecures) {
this.signSecures.addAll(signSecures);
return this;
}
/**
* 设置是否开启令牌严格模式
*/
public SecureRegistry strictToken(boolean strictToken) {
this.strictToken = strictToken;
return this;
}
/**
* 设置是否开启请求头严格模式
*/
public SecureRegistry strictHeader(boolean strictHeader) {
this.strictHeader = strictHeader;
return this;
}
/**
* 开启令牌严格模式
*/
public SecureRegistry strictTokenEnabled() {
this.strictToken = true;
return this;
}
/**
* 关闭令牌严格模式
*/
public SecureRegistry strictTokenDisabled() {
this.strictToken = false;
return this;
}
/**
* 开启请求头严格模式
*/
public SecureRegistry strictHeaderEnabled() {
this.strictHeader = true;
return this;
}
/**
* 关闭请求头严格模式
*/
public SecureRegistry strictHeaderDisabled() {
this.strictHeader = false;
return this;
}
/**
* 开启鉴权
*/
public SecureRegistry enabled() {
this.enabled = true;
return this;
}
/**
* 关闭鉴权
*/
public SecureRegistry disabled() {
this.enabled = false;
return this;
}
/**
* 开启授权
*/
public SecureRegistry authEnabled() {
this.authEnabled = true;
return this;
}
/**
* 关闭授权
*/
public SecureRegistry authDisabled() {
this.authEnabled = false;
return this;
}
/**
* 开启基础认证
*/
public SecureRegistry basicEnabled() {
this.basicEnabled = true;
return this;
}
/**
* 关闭基础认证
*/
public SecureRegistry basicDisabled() {
this.basicEnabled = false;
return this;
}
/**
* 开启签名认证
*/
public SecureRegistry signEnabled() {
this.signEnabled = true;
return this;
}
/**
* 关闭签名认证
*/
public SecureRegistry signDisabled() {
this.signEnabled = false;
return this;
}
/**
* 开启客户端认证
*/
public SecureRegistry clientEnabled() {
this.clientEnabled = true;
return this;
}
/**
* 关闭客户端认证
*/
public SecureRegistry clientDisabled() {
this.clientEnabled = false;
return this;
}
}
@@ -0,0 +1,73 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.resolver;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.secure.BladeUser;
import org.springblade.core.secure.utils.AuthUtil;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Token转化BladeUser
*
* @author Chill
*/
@Slf4j
public class TokenArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 入参筛选
*
* @param methodParameter 参数集合
* @return 格式化后的参数
*/
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterType().equals(BladeUser.class);
}
/**
* 出参设置
*
* @param methodParameter 入参集合
* @param modelAndViewContainer model 和 view
* @param nativeWebRequest web相关
* @param webDataBinderFactory 入参解析
* @return 包装对象
*/
@Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) {
return AuthUtil.getUser();
}
}
@@ -0,0 +1,201 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.core.secure.utils;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import lombok.SneakyThrows;
import org.springblade.core.jwt.JwtUtil;
import org.springblade.core.secure.TokenInfo;
import org.springblade.core.secure.constant.SecureConstant;
import org.springblade.core.tool.support.Kv;
import org.springblade.core.tool.utils.Charsets;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.WebUtil;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import static org.springblade.core.secure.constant.SecureConstant.*;
/**
* Secure工具类
*
* @author Chill
*/
public class SecureUtil extends AuthUtil {
public static final String TYP = "typ";
public static final String JWT = "JWT";
public static final String AUDIENCE = "bladex";
public static final String ISSUER = "bladex.cn";
/**
* 创建令牌
*
* @param kv 构建参数
* @return TokenInfo
*/
public static TokenInfo createToken(Kv kv) {
return createToken(kv, null, AUDIENCE, ISSUER);
}
/**
* 创建令牌
*
* @param kv 构建参数
* @param expire 过期秒数
* @return TokenInfo
*/
public static TokenInfo createToken(Kv kv, Integer expire) {
return createToken(kv, expire, AUDIENCE, ISSUER);
}
/**
* 创建令牌
*
* @param kv 构建参数
* @param expire 过期秒数
* @param audience audience
* @param issuer issuer
* @return TokenInfo
*/
public static TokenInfo createToken(Kv kv, Integer expire, String audience, String issuer) {
// 添加Token当前时间
Instant now = Instant.now();
// 获取过期时间
int expireSeconds = Optional.ofNullable(expire)
.orElseGet(SecureUtil::getExpire); // 获取默认过期时间
// 过期时间
Instant exp = now.plus(expireSeconds, ChronoUnit.SECONDS);
// 生效时间
Instant nbf = now.minus(60, ChronoUnit.SECONDS);
// 添加构成JWT的类
JwtBuilder builder = Jwts.builder().header().add(TYP, JWT)
.and().issuer(issuer).audience().add(audience)
.and().signWith(JwtUtil.getSecretKey());
// 设置JWT参数
kv.forEach(builder::claim);
// 设置Token认证时间
builder.expiration(Date.from(exp)).notBefore(Date.from(nbf));
// 组装Token信息
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setToken(builder.compact());
tokenInfo.setExpire(expireSeconds);
// 返回Token信息
return tokenInfo;
}
/**
* 获取默认过期时间(次日凌晨3点)
*
* @return expire
*/
public static int getExpire() {
LocalTime threeAM = LocalTime.of(3, 0);
LocalDate tomorrow = LocalDate.now(ZoneId.systemDefault()).plusDays(1);
Instant threeAMTomorrow = tomorrow.atTime(threeAM).atZone(ZoneId.systemDefault()).toInstant();
return (int) ChronoUnit.SECONDS.between(Instant.now(), threeAMTomorrow);
}
/**
* 获取请求头中的客户端id
*/
public static String getClientId() {
String[] tokens = extractAndDecodeAuthorization();
assert tokens.length == 2;
return tokens[0];
}
/**
* 获取请求头中的客户端密钥
*/
public static String getClientSecret() {
String[] tokens = extractAndDecodeAuthorization();
assert tokens.length == 2;
return tokens[1];
}
/**
* 客户端信息解码
*/
@SneakyThrows
public static String[] extractAndDecodeAuthorization() {
// 获取请求头客户端信息
String header = Objects.requireNonNull(WebUtil.getRequest()).getHeader(SecureConstant.BASIC_HEADER_KEY);
header = Func.toStr(header).replace(SecureConstant.BASIC_HEADER_PREFIX_EXT, SecureConstant.BASIC_HEADER_PREFIX);
if (!header.startsWith(SecureConstant.BASIC_HEADER_PREFIX)) {
throw new SecurityException(AUTHORIZATION_NOT_FOUND);
}
byte[] base64Token = header.substring(6).getBytes(Charsets.UTF_8_NAME);
byte[] decoded;
try {
decoded = Base64.getDecoder().decode(base64Token);
} catch (IllegalArgumentException exception) {
throw new SecurityException(CLIENT_TOKEN_PARSE_FAILED);
}
String token = new String(decoded, Charsets.UTF_8_NAME);
int index = token.indexOf(StringPool.COLON);
if (index == -1) {
throw new SecurityException(INVALID_CLIENT_TOKEN);
} else {
return new String[]{token.substring(0, index), token.substring(index + 1)};
}
}
/**
* 获取随机盐
*/
public static String createSalt() {
SecureRandom random = new SecureRandom();
byte[] saltBytes = new byte[16];
random.nextBytes(saltBytes);
StringBuilder sb = new StringBuilder();
for (byte b : saltBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}