From 49c4604f5eed4016edad32612c7a4244c8357038 Mon Sep 17 00:00:00 2001
From: guowei <3306787021@qq.com>
Date: Wed, 4 Sep 2024 21:53:36 +0800
Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E6=AC=A1=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 61 ++++++-
.../java/com/gw/test/TestApplication.java | 3 +
.../java/com/gw/test/config/CorsConfig.java | 23 +++
.../com/gw/test/config/IgnoreUrlsConfig.java | 21 +++
.../gw/test/config/SecurityBeanConfig.java | 61 +++++++
.../com/gw/test/config/SecurityConfig.java | 94 ++++++++++
.../gw/test/config/UserSecurityConfig.java | 50 ++++++
src/main/java/com/gw/test/domain/Result.java | 109 ++++++++++++
.../java/com/gw/test/domain/vo/LoginVo.java | 12 ++
src/main/java/com/gw/test/entity/User.java | 32 ++++
.../java/com/gw/test/enums/IErrorCode.java | 17 ++
.../java/com/gw/test/enums/ResultCode.java | 35 ++++
.../filters/DynamicAccessDecisionManager.java | 51 ++++++
.../test/filters/DynamicSecurityFilter.java | 85 ++++++++++
.../DynamicSecurityMetadataSource.java | 68 ++++++++
.../test/filters/DynamicSecurityService.java | 15 ++
.../filters/JwtAuthenticationTokenFilter.java | 57 +++++++
.../filters/RestAuthenticationEntryPoint.java | 30 ++++
.../filters/RestfulAccessDeniedHandler.java | 28 +++
src/main/java/com/gw/test/utils/JWTUtil.java | 160 ++++++++++++++++++
src/main/resources/application.properties | 1 -
src/main/resources/application.yml | 56 ++++++
22 files changed, 1066 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/com/gw/test/config/CorsConfig.java
create mode 100644 src/main/java/com/gw/test/config/IgnoreUrlsConfig.java
create mode 100644 src/main/java/com/gw/test/config/SecurityBeanConfig.java
create mode 100644 src/main/java/com/gw/test/config/SecurityConfig.java
create mode 100644 src/main/java/com/gw/test/config/UserSecurityConfig.java
create mode 100644 src/main/java/com/gw/test/domain/Result.java
create mode 100644 src/main/java/com/gw/test/domain/vo/LoginVo.java
create mode 100644 src/main/java/com/gw/test/entity/User.java
create mode 100644 src/main/java/com/gw/test/enums/IErrorCode.java
create mode 100644 src/main/java/com/gw/test/enums/ResultCode.java
create mode 100644 src/main/java/com/gw/test/filters/DynamicAccessDecisionManager.java
create mode 100644 src/main/java/com/gw/test/filters/DynamicSecurityFilter.java
create mode 100644 src/main/java/com/gw/test/filters/DynamicSecurityMetadataSource.java
create mode 100644 src/main/java/com/gw/test/filters/DynamicSecurityService.java
create mode 100644 src/main/java/com/gw/test/filters/JwtAuthenticationTokenFilter.java
create mode 100644 src/main/java/com/gw/test/filters/RestAuthenticationEntryPoint.java
create mode 100644 src/main/java/com/gw/test/filters/RestfulAccessDeniedHandler.java
create mode 100644 src/main/java/com/gw/test/utils/JWTUtil.java
delete mode 100644 src/main/resources/application.properties
create mode 100644 src/main/resources/application.yml
diff --git a/pom.xml b/pom.xml
index 90a3f69..e98dbda 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,9 +31,61 @@
- org.springframework.boot
- spring-boot-starter-jdbc
+ javax.xml.bind
+ jaxb-api
+ 2.3.1
+
+
+
+
+ com.mysql
+ mysql-connector-j
+
+
+
+
+ com.baomidou
+ mybatis-plus-spring-boot3-starter
+ 3.5.7
+
+
+
+
+ cn.hutool
+ hutool-all
+ 5.8.11
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+ 2.5.5
+
+
+
+ io.jsonwebtoken
+ jjwt
+ 0.9.1
+
+
+
+ com.github.xiaoymin
+ knife4j-openapi3-jakarta-spring-boot-starter
+ 4.5.0
+
+
+
+
+
+
+
+
+
+
+
+
org.springframework.boot
spring-boot-starter-web
@@ -49,6 +101,11 @@
spring-boot-starter-test
test
+
+ jakarta.annotation
+ jakarta.annotation-api
+ 2.1.1
+
diff --git a/src/main/java/com/gw/test/TestApplication.java b/src/main/java/com/gw/test/TestApplication.java
index ed1ca90..8024c26 100644
--- a/src/main/java/com/gw/test/TestApplication.java
+++ b/src/main/java/com/gw/test/TestApplication.java
@@ -1,9 +1,12 @@
package com.gw.test;
+import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
+@MapperScan("com.gw.test.mapper")
public class TestApplication {
public static void main(String[] args) {
diff --git a/src/main/java/com/gw/test/config/CorsConfig.java b/src/main/java/com/gw/test/config/CorsConfig.java
new file mode 100644
index 0000000..c97b04b
--- /dev/null
+++ b/src/main/java/com/gw/test/config/CorsConfig.java
@@ -0,0 +1,23 @@
+package com.gw.test.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ // 设置允许跨域请求的域名
+ .allowedOriginPatterns("*")
+ // 是否允许cookie
+ .allowCredentials(true)
+ // 设置允许的请求方式
+ .allowedMethods("GET", "POST", "DELETE", "PUT")
+ // 设置允许的header属性
+ .allowedHeaders("*")
+ // 跨域允许时间
+ .maxAge(3600);
+ }
+}
diff --git a/src/main/java/com/gw/test/config/IgnoreUrlsConfig.java b/src/main/java/com/gw/test/config/IgnoreUrlsConfig.java
new file mode 100644
index 0000000..8e907ad
--- /dev/null
+++ b/src/main/java/com/gw/test/config/IgnoreUrlsConfig.java
@@ -0,0 +1,21 @@
+package com.gw.test.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * SpringSecurity白名单资源路径配置
+ * Created by macro on 2018/11/5.
+ */
+@Data
+@Component
+@ConfigurationProperties(prefix = "secure.ignored")
+public class IgnoreUrlsConfig {
+
+ private List urls = new ArrayList<>();
+
+}
diff --git a/src/main/java/com/gw/test/config/SecurityBeanConfig.java b/src/main/java/com/gw/test/config/SecurityBeanConfig.java
new file mode 100644
index 0000000..18186ea
--- /dev/null
+++ b/src/main/java/com/gw/test/config/SecurityBeanConfig.java
@@ -0,0 +1,61 @@
+package com.gw.test.config;
+
+import com.gw.test.filters.*;
+import com.gw.test.utils.JWTUtil;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+
+@Configuration
+public class SecurityBeanConfig {
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+// @Bean
+// public IgnoreUrlsConfig ignoreUrlsConfig() {
+// return new IgnoreUrlsConfig();
+// }
+
+ @Bean
+ public JWTUtil jWTUtil() {
+ return new JWTUtil();
+ }
+
+ @Bean
+ public RestfulAccessDeniedHandler restfulAccessDeniedHandler() {
+ return new RestfulAccessDeniedHandler();
+ }
+
+ @Bean
+ public RestAuthenticationEntryPoint restAuthenticationEntryPoint() {
+ return new RestAuthenticationEntryPoint();
+ }
+
+ @Bean
+ public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(UserDetailsService userDetailsService, JWTUtil jwtTokenUtil){
+ return new JwtAuthenticationTokenFilter(userDetailsService,jwtTokenUtil);
+ }
+
+ @ConditionalOnBean(name = "dynamicSecurityService")
+ @Bean
+ public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
+ return new DynamicAccessDecisionManager();
+ }
+
+ @ConditionalOnBean(name = "dynamicSecurityService")
+ @Bean
+ public DynamicSecurityMetadataSource dynamicSecurityMetadataSource(DynamicSecurityService dynamicSecurityService) {
+ return new DynamicSecurityMetadataSource(dynamicSecurityService);
+ }
+
+ @ConditionalOnBean(name = "dynamicSecurityService")
+ @Bean
+ public DynamicSecurityFilter dynamicSecurityFilter(DynamicSecurityMetadataSource dynamicSecurityMetadataSource,IgnoreUrlsConfig ignoreUrlsConfig){
+ return new DynamicSecurityFilter(dynamicSecurityMetadataSource,ignoreUrlsConfig);
+ }
+}
diff --git a/src/main/java/com/gw/test/config/SecurityConfig.java b/src/main/java/com/gw/test/config/SecurityConfig.java
new file mode 100644
index 0000000..99d9c02
--- /dev/null
+++ b/src/main/java/com/gw/test/config/SecurityConfig.java
@@ -0,0 +1,94 @@
+package com.gw.test.config;
+
+import com.gw.test.filters.*;
+import com.gw.test.utils.JWTUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+@Slf4j
+@Configuration
+@EnableWebSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+ private final JWTUtil jwtUtil;
+
+ private final IgnoreUrlsConfig ignoreUrlsConfig;
+
+ private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
+
+ private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
+
+ private final JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
+
+// @Autowired(required = false)
+// private DynamicSecurityService dynamicSecurityService;
+
+ @Autowired(required = false)
+ private DynamicSecurityFilter dynamicSecurityFilter;
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
+ ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
+ //不需要保护的资源路径允许访问
+ for (String url : ignoreUrlsConfig.getUrls()) {
+// log.error("[{}] [{}]", "白名单路径", url);
+ registry.requestMatchers(url).permitAll();
+ }
+ //允许跨域请求的OPTIONS请求
+ registry.requestMatchers(HttpMethod.OPTIONS)
+ .permitAll();
+ // 任何请求需要身份认证
+ registry.and()
+ .authorizeRequests()
+ .anyRequest()
+ .authenticated()
+ // 关闭跨站请求防护及不使用session
+ .and()
+ .csrf(AbstractHttpConfigurer::disable)
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .exceptionHandling(exception -> exception
+ .accessDeniedHandler(restfulAccessDeniedHandler)
+ .authenticationEntryPoint(restAuthenticationEntryPoint))
+ // 自定义JWT认证过滤器
+ .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+ // 添加动态权限校验过滤器
+// .addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
+ return httpSecurity.build();
+ }
+//
+// @Bean
+// public PasswordEncoder passwordEncoder() {
+// // 密码加密方式
+// return new BCryptPasswordEncoder();
+// }
+
+// @Bean
+// public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
+// .exceptionHandling()
+// .accessDeniedHandler(restfulAccessDeniedHandler)
+// .authenticationEntryPoint(restAuthenticationEntryPoint)
+// // 自定义权限拦截器JWT过滤器
+// .and()
+// .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
+ //有动态权限配置时添加动态权限校验过滤器
+// if(dynamicSecurityService != null){
+// registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
+// }
+// }
+}
diff --git a/src/main/java/com/gw/test/config/UserSecurityConfig.java b/src/main/java/com/gw/test/config/UserSecurityConfig.java
new file mode 100644
index 0000000..02a9aa5
--- /dev/null
+++ b/src/main/java/com/gw/test/config/UserSecurityConfig.java
@@ -0,0 +1,50 @@
+package com.gw.test.config;
+
+import com.gw.test.filters.DynamicSecurityService;
+import com.gw.test.service.IGuoWeiService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.access.SecurityConfig;
+import org.springframework.security.core.userdetails.UserDetailsService;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 后台用户安全配置
+ */
+@RequiredArgsConstructor
+@Configuration
+public class UserSecurityConfig {
+
+ private final IGuoWeiService service;
+
+// private final IApiService apiService;
+
+ @Bean
+ public UserDetailsService userDetailsService() {
+ //获取登录用户信息
+ return service::loadUserByUsername;
+ }
+
+ @Bean
+ public DynamicSecurityService dynamicSecurityService() {
+ return () -> {
+ Map map = new ConcurrentHashMap<>();
+ Map apis = new HashMap<>();
+ apis.put("查询用户","ROLE_USER");
+ apis.put("新增用户","ROLE_ADMIN");
+ apis.put("删除用户","ROLE_ADMIN");
+ apis.put("修改用户","ROLE_ADMIN");
+ for (String api : apis.keySet()) {
+ map.put(api,new SecurityConfig(apis.get(api)));
+ }
+ return map;
+ };
+ }
+}
diff --git a/src/main/java/com/gw/test/domain/Result.java b/src/main/java/com/gw/test/domain/Result.java
new file mode 100644
index 0000000..822ca89
--- /dev/null
+++ b/src/main/java/com/gw/test/domain/Result.java
@@ -0,0 +1,109 @@
+package com.gw.test.domain;
+
+import com.gw.test.enums.ResultCode;
+import lombok.Data;
+
+/**
+ * 定义一个R类,用于封装响应数据
+ * @since 2021-09-04
+ */
+@Data
+public class Result {
+ /**
+ * 响应码
+ */
+ private int code;
+ /**
+ * 响应消息
+ */
+ private String msg;
+ /**
+ * 响应数据
+ */
+ private T data;
+
+ /**
+ * 返回一个成功的响应
+ */
+ public static Result ok() {
+ return ok(null);
+ }
+
+ /**
+ * 返回一个成功的响应,带有消息和数据
+ * @param msg 消息内容
+ * @param data 数据
+ * @return 成功响应
+ */
+ public static Result ok(String msg,T data){
+ return new Result<>(200, msg, data);
+ }
+
+ public static Result unauthorized(T data){
+ return new Result<>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
+ }
+
+ public static Result forbidden(T data){
+ return new Result<>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
+ }
+
+
+ /**
+ * 返回一个成功的响应,仅带有数据
+ * @param data 数据
+ * @return 成功响应
+ */
+ public static Result ok(T data) {
+ return new Result<>(200, "OK", data);
+ }
+
+// public static R ok(T data,String msg) {
+// return new R<>(200, msg, data);
+// }
+
+ /**
+ * 返回一个错误的响应
+ * @param msg 消息内容
+ * @return 错误响应
+ */
+ public static Result error(String msg) {
+ return new Result<>(500, msg, null);
+ }
+
+ /**
+ * 返回一个错误的响应,带有错误码和消息
+ * @param code 错误码
+ * @param msg 消息内容
+ * @return 错误响应
+ */
+ public static Result error(int code, String msg) {
+ return new Result<>(code, msg, null);
+ }
+
+
+ /**
+ * 构造函数
+ */
+ public Result() {
+ }
+
+ /**
+ * 构造函数,带有参数
+ * @param code 响应码
+ * @param msg 响应消息
+ * @param data 响应数据
+ */
+ public Result(int code, String msg, T data) {
+ this.code = code;
+ this.msg = msg;
+ this.data = data;
+ }
+
+ /**
+ * 判断响应是否成功
+ * @return true-成功,false-失败
+ */
+ public boolean success(){
+ return code == 200;
+ }
+}
diff --git a/src/main/java/com/gw/test/domain/vo/LoginVo.java b/src/main/java/com/gw/test/domain/vo/LoginVo.java
new file mode 100644
index 0000000..c87f40c
--- /dev/null
+++ b/src/main/java/com/gw/test/domain/vo/LoginVo.java
@@ -0,0 +1,12 @@
+package com.gw.test.domain.vo;
+
+import lombok.Data;
+
+@Data
+public class LoginVo {
+ private int id;
+
+ private String username;
+
+ private String token;
+}
diff --git a/src/main/java/com/gw/test/entity/User.java b/src/main/java/com/gw/test/entity/User.java
new file mode 100644
index 0000000..2c286d4
--- /dev/null
+++ b/src/main/java/com/gw/test/entity/User.java
@@ -0,0 +1,32 @@
+package com.gw.test.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+public class User implements UserDetails {
+ private GuoWei entity;
+
+ private List authorities;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return this.authorities;
+ }
+
+ @Override
+ public String getPassword() {
+ return entity.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return entity.getUsername();
+ }
+}
diff --git a/src/main/java/com/gw/test/enums/IErrorCode.java b/src/main/java/com/gw/test/enums/IErrorCode.java
new file mode 100644
index 0000000..7d7ae4d
--- /dev/null
+++ b/src/main/java/com/gw/test/enums/IErrorCode.java
@@ -0,0 +1,17 @@
+package com.gw.test.enums;
+
+/**
+ * API返回码接口
+ * Created by macro on 2019/4/19.
+ */
+public interface IErrorCode {
+ /**
+ * 返回码
+ */
+ int getCode();
+
+ /**
+ * 返回信息
+ */
+ String getMessage();
+}
diff --git a/src/main/java/com/gw/test/enums/ResultCode.java b/src/main/java/com/gw/test/enums/ResultCode.java
new file mode 100644
index 0000000..388cf19
--- /dev/null
+++ b/src/main/java/com/gw/test/enums/ResultCode.java
@@ -0,0 +1,35 @@
+package com.gw.test.enums;
+
+import lombok.Data;
+import lombok.Getter;
+
+/**
+ * API返回码封装类
+ * Created by macro on 2019/4/19.
+ */
+//@Data
+public enum ResultCode implements IErrorCode {
+ SUCCESS(200, "操作成功"),
+ FAILED(500, "操作失败"),
+ VALIDATE_FAILED(404, "参数检验失败"),
+ UNAUTHORIZED(401, "暂未登录或token已经过期"),
+ FORBIDDEN(403, "没有相关权限");
+ private final int code;
+ private final String message;
+
+ private ResultCode(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+
+ @Override
+ public int getCode() {
+ return this.code;
+ }
+
+ @Override
+ public String getMessage() {
+ return this.message;
+ }
+}
diff --git a/src/main/java/com/gw/test/filters/DynamicAccessDecisionManager.java b/src/main/java/com/gw/test/filters/DynamicAccessDecisionManager.java
new file mode 100644
index 0000000..051a272
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/DynamicAccessDecisionManager.java
@@ -0,0 +1,51 @@
+package com.gw.test.filters;
+
+
+import org.springframework.security.access.AccessDecisionManager;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.authentication.InsufficientAuthenticationException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/**
+ * 判断用户是否有访问权限
+ */
+public class DynamicAccessDecisionManager implements AccessDecisionManager {
+
+ @Override
+ public void decide(Authentication authentication, Object object,
+ Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
+ // 当接口未被配置资源时直接放行
+// if (CollUtil.isEmpty(configAttributes)) {
+// return;
+// }
+ if (configAttributes== null || configAttributes.isEmpty()) {
+ return;
+ }
+ for (ConfigAttribute configAttribute : configAttributes) {
+ //将访问所需资源或用户拥有资源进行比对
+ String needAuthority = configAttribute.getAttribute();
+ for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
+ if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
+ return;
+ }
+ }
+ }
+ throw new AccessDeniedException("抱歉,您没有访问权限");
+ }
+
+ @Override
+ public boolean supports(ConfigAttribute configAttribute) {
+ return true;
+ }
+
+ @Override
+ public boolean supports(Class> aClass) {
+ return true;
+ }
+
+}
diff --git a/src/main/java/com/gw/test/filters/DynamicSecurityFilter.java b/src/main/java/com/gw/test/filters/DynamicSecurityFilter.java
new file mode 100644
index 0000000..d72c623
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/DynamicSecurityFilter.java
@@ -0,0 +1,85 @@
+package com.gw.test.filters;
+
+//import com.gw.security.config.IgnoreUrlsConfig;
+//import jakarta.servlet.Filter;
+import com.gw.test.config.IgnoreUrlsConfig;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.access.SecurityMetadataSource;
+import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
+import org.springframework.security.access.intercept.InterceptorStatusToken;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+import jakarta.servlet.*;
+//import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * 权限过滤器,实现基于路径的动态权限过滤
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
+
+
+ private final DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
+
+
+ private final IgnoreUrlsConfig ignoreUrlsConfig;
+
+ @Autowired
+ public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
+ super.setAccessDecisionManager(dynamicAccessDecisionManager);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+ FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
+ //OPTIONS请求直接放行
+ if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
+ fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+ return;
+ }
+ //白名单请求直接放行
+ PathMatcher pathMatcher = new AntPathMatcher();
+ for (String path : ignoreUrlsConfig.getUrls()) {
+ log.error("=============>ignor:{},{}",path,request.getRequestURI());
+ if(pathMatcher.match(path,request.getRequestURI())){
+ fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+ return;
+ }
+ }
+ //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
+ InterceptorStatusToken token = super.beforeInvocation(fi);
+ try {
+ fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
+ } finally {
+ super.afterInvocation(token, null);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+ @Override
+ public Class> getSecureObjectClass() {
+ return FilterInvocation.class;
+ }
+
+ @Override
+ public SecurityMetadataSource obtainSecurityMetadataSource() {
+ return dynamicSecurityMetadataSource;
+ }
+
+}
diff --git a/src/main/java/com/gw/test/filters/DynamicSecurityMetadataSource.java b/src/main/java/com/gw/test/filters/DynamicSecurityMetadataSource.java
new file mode 100644
index 0000000..c24aa4f
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/DynamicSecurityMetadataSource.java
@@ -0,0 +1,68 @@
+package com.gw.test.filters;
+
+import cn.hutool.core.util.URLUtil;
+import jakarta.annotation.PostConstruct;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.ConfigAttribute;
+import org.springframework.security.web.FilterInvocation;
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.PathMatcher;
+
+import java.util.*;
+
+/**
+ * 动态权限数据源,获取动态权限规则
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
+
+ private static Map configAttributeMap = null;
+// @Autowired
+ private final DynamicSecurityService dynamicSecurityService;
+
+ @PostConstruct
+ public void loadDataSource() {
+ configAttributeMap = dynamicSecurityService.loadDataSource();
+ }
+
+ public void clearDataSource() {
+ configAttributeMap.clear();
+ configAttributeMap = null;
+ }
+
+ @Override
+ public Collection getAttributes(Object o) throws IllegalArgumentException {
+ if (configAttributeMap == null) this.loadDataSource();
+ List configAttributes = new ArrayList<>();
+ //获取当前访问的路径
+ String url = ((FilterInvocation) o).getRequestUrl();
+ String path = URLUtil.getPath(url);
+ log.error("------DynamicSecurityMetadataSource:请求url:{}",url);
+ PathMatcher pathMatcher = new AntPathMatcher();
+ //获取访问该路径所需资源
+ for (String pattern : configAttributeMap.keySet()) {
+ if (pathMatcher.match(pattern, path)) {
+ log.error("------DynamicSecurityMetadataSource:匹配到url:{}:{}",pattern,configAttributeMap.get(pattern));
+ configAttributes.add(configAttributeMap.get(pattern));
+ }
+ }
+ // 未设置操作请求权限,返回空集合
+ return configAttributes;
+ }
+
+ @Override
+ public Collection getAllConfigAttributes() {
+ return null;
+ }
+
+ @Override
+ public boolean supports(Class> aClass) {
+ return true;
+ }
+
+}
diff --git a/src/main/java/com/gw/test/filters/DynamicSecurityService.java b/src/main/java/com/gw/test/filters/DynamicSecurityService.java
new file mode 100644
index 0000000..1a38eaa
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/DynamicSecurityService.java
@@ -0,0 +1,15 @@
+package com.gw.test.filters;
+
+import org.springframework.security.access.ConfigAttribute;
+
+import java.util.Map;
+
+/**
+ * 动态权限相关业务接口
+ */
+public interface DynamicSecurityService {
+ /**
+ * 加载资源ANT通配符和资源对应MAP
+ */
+ Map loadDataSource();
+}
diff --git a/src/main/java/com/gw/test/filters/JwtAuthenticationTokenFilter.java b/src/main/java/com/gw/test/filters/JwtAuthenticationTokenFilter.java
new file mode 100644
index 0000000..e0ceab2
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/JwtAuthenticationTokenFilter.java
@@ -0,0 +1,57 @@
+package com.gw.test.filters;
+
+import com.gw.test.utils.JWTUtil;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * JWT登录授权过滤器
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
+// private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
+
+ private final UserDetailsService userDetailsService;
+
+ private final JWTUtil jwtTokenUtil;
+
+ @Value("${jwt.tokenHeader}")
+ private String tokenHeader;
+
+ @Value("${jwt.tokenHead}")
+ private String tokenHead;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain chain) throws ServletException, IOException {
+ String authHeader = request.getHeader(this.tokenHeader);
+ if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
+ String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer "
+ String username = jwtTokenUtil.getUserNameFromToken(authToken);
+ if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
+ UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
+ if (jwtTokenUtil.validateToken(authToken, userDetails)) {
+ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
+ authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ }
+ }
+ }
+ chain.doFilter(request, response);
+ }
+}
diff --git a/src/main/java/com/gw/test/filters/RestAuthenticationEntryPoint.java b/src/main/java/com/gw/test/filters/RestAuthenticationEntryPoint.java
new file mode 100644
index 0000000..f0b46f4
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/RestAuthenticationEntryPoint.java
@@ -0,0 +1,30 @@
+package com.gw.test.filters;
+
+import cn.hutool.json.JSONUtil;
+import com.gw.test.domain.Result;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import java.io.IOException;
+
+/**
+ * 未登录或者token失效时的返回结果
+ */
+@Slf4j
+public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Cache-Control","no-cache");
+// response.setHeader("authorization", authException.getMessage());
+// log.error("code{},msg:{}", ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage());
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+ response.getWriter().println(JSONUtil.parse(Result.unauthorized(authException.getMessage())));
+ response.getWriter().flush();
+ }
+}
diff --git a/src/main/java/com/gw/test/filters/RestfulAccessDeniedHandler.java b/src/main/java/com/gw/test/filters/RestfulAccessDeniedHandler.java
new file mode 100644
index 0000000..64b8b1e
--- /dev/null
+++ b/src/main/java/com/gw/test/filters/RestfulAccessDeniedHandler.java
@@ -0,0 +1,28 @@
+package com.gw.test.filters;
+
+import cn.hutool.json.JSONUtil;
+import com.gw.test.domain.Result;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+
+import java.io.IOException;
+
+/**
+ * 自定义无权限访问的返回结果
+ */
+public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
+ @Override
+ public void handle(HttpServletRequest request,
+ HttpServletResponse response,
+ AccessDeniedException e) throws IOException, ServletException {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.setHeader("Cache-Control","no-cache");
+ response.setCharacterEncoding("UTF-8");
+ response.setContentType("application/json");
+ response.getWriter().println(JSONUtil.parse(Result.forbidden(e.getMessage()+"没有权限访问该资源....")));
+ response.getWriter().flush();
+ }
+}
diff --git a/src/main/java/com/gw/test/utils/JWTUtil.java b/src/main/java/com/gw/test/utils/JWTUtil.java
new file mode 100644
index 0000000..bd7846b
--- /dev/null
+++ b/src/main/java/com/gw/test/utils/JWTUtil.java
@@ -0,0 +1,160 @@
+package com.gw.test.utils;
+
+
+import cn.hutool.core.date.DateUtil;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class JWTUtil {
+// private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);
+ private static final String CLAIM_KEY_USERNAME = "sub";
+ private static final String CLAIM_KEY_CREATED = "created";
+ @Value("${jwt.secret}")
+ private String secret;
+ @Value("${jwt.expiration}")
+ private Long expiration;
+ @Value("${jwt.tokenHead}")
+ private String tokenHead;
+
+ /**
+ * 根据负责生成JWT的token
+ */
+ private String generateToken(Map claims) {
+ return Jwts.builder()
+ .setClaims(claims)
+ .setExpiration(generateExpirationDate())
+ .signWith(SignatureAlgorithm.HS512, secret)
+ .compact();
+ }
+
+ /**
+ * 从token中获取JWT中的负载
+ */
+ private Claims getClaimsFromToken(String token) {
+ Claims claims = null;
+ try {
+ claims = Jwts.parser()
+ .setSigningKey(secret)
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (Exception e) {
+ log.info("JWT格式验证失败:{}", token);
+ }
+ return claims;
+ }
+
+ /**
+ * 生成token的过期时间
+ */
+ private Date generateExpirationDate() {
+ return new Date(System.currentTimeMillis() + expiration * 1000);
+ }
+
+ /**
+ * 从token中获取登录用户名
+ */
+ public String getUserNameFromToken(String token) {
+ String username;
+ try {
+ Claims claims = getClaimsFromToken(token);
+ username = claims.getSubject();
+ } catch (Exception e) {
+ username = null;
+ }
+ return username;
+ }
+
+ /**
+ * 验证token是否还有效
+ *
+ * @param token 客户端传入的token
+ * @param userDetails 从数据库中查询出来的用户信息
+ */
+ public boolean validateToken(String token, UserDetails userDetails) {
+ if (userDetails == null || token == null)
+ return false;
+ String username = getUserNameFromToken(token);
+ return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
+ }
+
+ /**
+ * 判断token是否已经失效
+ */
+ private boolean isTokenExpired(String token) {
+ Date expiredDate = getExpiredDateFromToken(token);
+ return expiredDate.before(new Date());
+ }
+
+ /**
+ * 从token中获取过期时间
+ */
+ private Date getExpiredDateFromToken(String token) {
+ Claims claims = getClaimsFromToken(token);
+ return claims.getExpiration();
+ }
+
+ /**
+ * 根据用户信息生成token
+ */
+ public String generateToken(UserDetails userDetails) {
+ Map claims = new HashMap<>();
+ claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername());
+ claims.put(CLAIM_KEY_CREATED, new Date());
+ return generateToken(claims);
+ }
+
+ /**
+ * 当原来的token没过期时是可以刷新的
+ *
+ * @param oldToken 带tokenHead的token
+ */
+ public String refreshHeadToken(String oldToken) {
+// if(StrUtil.isEmpty(oldToken)){
+// return null;
+// }
+ if(oldToken == null || oldToken.isEmpty()) return null;
+ String token = oldToken.substring(tokenHead.length());
+// if(StrUtil.isEmpty(token)){
+// return null;
+// }
+ if(token.isEmpty()) return null;
+ //token校验不通过
+ Claims claims = getClaimsFromToken(token);
+ if(claims==null){
+ return null;
+ }
+ //如果token已经过期,不支持刷新
+ if(isTokenExpired(token)){
+ return null;
+ }
+ //如果token在30分钟之内刚刷新过,返回原token
+ if(tokenRefreshJustBefore(token,30*60)){
+ return token;
+ }else{
+ claims.put(CLAIM_KEY_CREATED, new Date());
+ return generateToken(claims);
+ }
+ }
+
+ /**
+ * 判断token在指定时间内是否刚刚刷新过
+ * @param token 原token
+ * @param time 指定时间(秒)
+ */
+ private boolean tokenRefreshJustBefore(String token, int time) {
+ Claims claims = getClaimsFromToken(token);
+ Date created = claims.get(CLAIM_KEY_CREATED, Date.class);
+ Date refreshDate = new Date();
+ //刷新时间在创建时间的指定时间内
+ return refreshDate.after(created) && refreshDate.before(DateUtil.offsetSecond(created, time));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 15edec8..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1 +0,0 @@
-spring.application.name=test
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..766ee9a
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,56 @@
+spring:
+ application:
+ name: test
+
+ datasource:
+ url: jdbc:mysql://222.186.15.41:3306/testapi
+ username: testapi
+ password: BYGAxwj4PZi4GZsA
+
+server:
+ port: 8080
+
+
+mybatis-plus:
+ global-config:
+ db-config:
+ column-format: "`%s`"
+ configuration:
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ map-underscore-to-camel-case: true
+
+knife4j:
+ enable: true #开启增强模式
+ setting:
+ language: zh_cn
+
+springdoc:
+ group-configs:
+ - group: 'test'
+ packages-to-scan: com.gw.test.controller
+ # 配置Swagger UI的访问路径和排序方式
+ swagger-ui:
+ path: /doc.html # Swagger UI的访问路径
+# tags-sorter: alpha # 按字母顺序排序标签
+# operations-sorter: alpha # 按字母顺序排序操作
+ # 配置API文档的访问路径
+ api-docs:
+ path: /docs # API文档的访问路径
+
+jwt:
+ tokenHeader: Authorization #JWT存储的请求头
+ secret: guowei #JWT加解密使用的密钥
+ expiration: 604800 #JWT的超期限时间(60*60*24*7)
+ tokenHead: 'Bearer ' #JWT负载中拿到开头
+
+secure:
+ ignored:
+ urls:
+ - /webjars/**
+ - /swagger-resources/**
+ - /v3/**
+ - /swagger-ui.html
+ - /docs
+ - /doc.html
+ - /guowei/login
+ - /guowei/register
\ No newline at end of file
--
2.54.0