!14 增加匿名访问控制

Merge pull request !14 from stackcn/master
This commit is contained in:
郝先瑞
2024-12-06 15:41:15 +00:00
committed by Gitee
11 changed files with 528 additions and 13 deletions

View File

@@ -0,0 +1,11 @@
package com.youlai.boot.common.annotation;
import java.lang.annotation.*;
/// 标记匿名访问
@Inherited
@Documented
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}

View File

@@ -0,0 +1,67 @@
package com.youlai.boot.common.annotation.methods;
import com.youlai.boot.common.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code DELETE} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 DeleteMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.DELETE)
public @interface AnonymousDeleteMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}

View File

@@ -0,0 +1,67 @@
package com.youlai.boot.common.annotation.methods;
import com.youlai.boot.common.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code GET} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 GetMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.GET)
public @interface AnonymousGetMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}

View File

@@ -0,0 +1,67 @@
package com.youlai.boot.common.annotation.methods;
import com.youlai.boot.common.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code PATCH} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 PatchMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PATCH)
public @interface AnonymousPatchMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}

View File

@@ -0,0 +1,67 @@
package com.youlai.boot.common.annotation.methods;
import com.youlai.boot.common.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code POST} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 PostMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.POST)
public @interface AnonymousPostMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}

View File

@@ -0,0 +1,67 @@
package com.youlai.boot.common.annotation.methods;
import com.youlai.boot.common.annotation.AnonymousAccess;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.lang.annotation.*;
/**
* Annotation for mapping HTTP {@code PUT} requests onto specific handler
* methods.
* <p>
* 支持匿名访问 PutMapping
*
* @see RequestMapping
*/
@AnonymousAccess
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RequestMapping(method = RequestMethod.PUT)
public @interface AnonymousPutMapping {
/**
* Alias for {@link RequestMapping#name}.
*/
@AliasFor(annotation = RequestMapping.class)
String name() default "";
/**
* Alias for {@link RequestMapping#value}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] value() default {};
/**
* Alias for {@link RequestMapping#path}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] path() default {};
/**
* Alias for {@link RequestMapping#params}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] params() default {};
/**
* Alias for {@link RequestMapping#headers}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] headers() default {};
/**
* Alias for {@link RequestMapping#consumes}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] consumes() default {};
/**
* Alias for {@link RequestMapping#produces}.
*/
@AliasFor(annotation = RequestMapping.class)
String[] produces() default {};
}

View File

@@ -0,0 +1,52 @@
package com.youlai.boot.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RequestMethodEnum {
/**
* 搜寻 @AnonymousGetMapping
*/
GET("GET"),
/**
* 搜寻 @AnonymousPostMapping
*/
POST("POST"),
/**
* 搜寻 @AnonymousPutMapping
*/
PUT("PUT"),
/**
* 搜寻 @AnonymousPatchMapping
*/
PATCH("PATCH"),
/**
* 搜寻 @AnonymousDeleteMapping
*/
DELETE("DELETE"),
/**
* 否则就是所有 Request 接口都放行
*/
ALL("All");
/**
* Request 类型
*/
private final String type;
public static RequestMethodEnum find(String type) {
for (RequestMethodEnum value : RequestMethodEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return ALL;
}
}

View File

@@ -0,0 +1,91 @@
package com.youlai.boot.common.util;
import com.youlai.boot.common.annotation.AnonymousAccess;
import com.youlai.boot.common.enums.RequestMethodEnum;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.*;
import java.util.stream.Collectors;
public class AnonymousUtils {
/**
* 获取所有匿名标记URL不区分请求方式
*/
public static Set<String> getAnonymousUrls(ApplicationContext applicationContext) {
return getAllAnonymousUrls(applicationContext).values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
}
/**
* 获取所有被标记的匿名类集合
*
* @return /
*/
public static Map<String, Set<String>> getAllAnonymousUrls(ApplicationContext applicationContext) {
// 搜索匿名标记
RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) applicationContext.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods();
// 获取所以被标记的匿名类集合
return getAnonymousUrl(handlerMethodMap);
}
/**
* 获取所有被标记的匿名类集合
*
* @param handlerMethodMap 请求映射信息集合
* @return /
*/
public static Map<String, Set<String>> getAnonymousUrl(Map<RequestMappingInfo, HandlerMethod> handlerMethodMap) {
Set<String> get = new HashSet<>();
Set<String> post = new HashSet<>();
Set<String> put = new HashSet<>();
Set<String> patch = new HashSet<>();
Set<String> delete = new HashSet<>();
Set<String> all = new HashSet<>();
handlerMethodMap.forEach((key, value) -> {
AnonymousAccess anonymousAccess = value.getMethodAnnotation(AnonymousAccess.class);
if (anonymousAccess != null) {
ArrayList<RequestMethod> requestMethods = new ArrayList<>(key.getMethodsCondition().getMethods());
RequestMethodEnum request = RequestMethodEnum.find(requestMethods.isEmpty() ? RequestMethodEnum.ALL.getType() : requestMethods.get(0).name());
switch (Objects.requireNonNull(request)) {
case GET:
get.addAll(key.getDirectPaths());
break;
case POST:
post.addAll(key.getDirectPaths());
break;
case PUT:
put.addAll(key.getDirectPaths());
break;
case PATCH:
patch.addAll(key.getDirectPaths());
break;
case DELETE:
delete.addAll(key.getDirectPaths());
break;
default:
all.addAll(key.getDirectPaths());
}
}
});
return Map.ofEntries(
entry(RequestMethodEnum.GET.getType(), get),
entry(RequestMethodEnum.POST.getType(), post),
entry(RequestMethodEnum.PUT.getType(), put),
entry(RequestMethodEnum.PATCH.getType(), patch),
entry(RequestMethodEnum.DELETE.getType(), delete),
entry(RequestMethodEnum.ALL.getType(), all)
);
}
public static Map.Entry<String, Set<String>> entry(String key, Collection<String> collection) {
return Map.entry(key, collection.stream().filter(it -> !it.isEmpty()).collect(Collectors.toUnmodifiableSet()));
}
}

View File

@@ -3,7 +3,8 @@ package com.youlai.boot.config;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.enums.RequestMethodEnum;
import com.youlai.boot.common.util.AnonymousUtils;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.core.filter.RateLimiterFilter; import com.youlai.boot.core.filter.RateLimiterFilter;
import com.youlai.boot.core.security.exception.MyAccessDeniedHandler; import com.youlai.boot.core.security.exception.MyAccessDeniedHandler;
@@ -16,9 +17,11 @@ import com.youlai.boot.shared.auth.service.impl.JwtTokenService;
import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
@@ -33,6 +36,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.util.Map;
import java.util.Set;
/** /**
* Spring Security 安全配置 * Spring Security 安全配置
* *
@@ -60,16 +66,30 @@ public class SecurityConfig {
private final MyAuthenticationEntryPoint authenticationEntryPoint; // 项目内安全类 private final MyAuthenticationEntryPoint authenticationEntryPoint; // 项目内安全类
private final MyAccessDeniedHandler accessDeniedHandler; private final MyAccessDeniedHandler accessDeniedHandler;
private final ApplicationContext applicationContext;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 获取所有匿名访问路径
Map<String, Set<String>> anonymousUrls = AnonymousUtils.getAllAnonymousUrls(applicationContext);
http http
.authorizeHttpRequests(requestMatcherRegistry -> .authorizeHttpRequests(requestMatcherRegistry ->
requestMatcherRegistry requestMatcherRegistry
.requestMatchers( // GET
SecurityConstants.LOGIN_PATH, .requestMatchers(HttpMethod.GET, anonymousUrls.get(RequestMethodEnum.GET.getType()).toArray(new String[0])).permitAll()
SecurityConstants.WECHAT_LOGIN_PATH) // POST
.permitAll() .requestMatchers(HttpMethod.POST, anonymousUrls.get(RequestMethodEnum.POST.getType()).toArray(new String[0])).permitAll()
// PUT
.requestMatchers(HttpMethod.PUT, anonymousUrls.get(RequestMethodEnum.PUT.getType()).toArray(new String[0])).permitAll()
// PATCH
.requestMatchers(HttpMethod.PATCH, anonymousUrls.get(RequestMethodEnum.PATCH.getType()).toArray(new String[0])).permitAll()
// DELETE
.requestMatchers(HttpMethod.DELETE, anonymousUrls.get(RequestMethodEnum.DELETE.getType()).toArray(new String[0])).permitAll()
// 所有类型的接口都放行
.requestMatchers(anonymousUrls.get(RequestMethodEnum.ALL.getType()).toArray(new String[0])).permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->

View File

@@ -7,13 +7,12 @@ import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil; import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.aliyun.oss.HttpMethod; import com.aliyun.oss.HttpMethod;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.util.AnonymousUtils;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.core.security.util.SecurityUtils;
import com.youlai.boot.system.model.entity.Log; import com.youlai.boot.system.model.entity.Log;
import com.youlai.boot.system.service.LogService; import com.youlai.boot.system.service.LogService;
import groovyjarjarpicocli.CommandLine;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -23,6 +22,7 @@ import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing; import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
@@ -31,6 +31,7 @@ import org.springframework.web.servlet.HandlerMapping;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* 日志切面 * 日志切面
@@ -43,9 +44,9 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class LogAspect { public class LogAspect {
private final LogService logService; private final LogService logService;
private final HttpServletRequest request; private final HttpServletRequest request;
private final ApplicationContext applicationContext;
@Pointcut("@annotation(com.youlai.boot.core.annotation.Log)") @Pointcut("@annotation(com.youlai.boot.core.annotation.Log)")
public void logPointcut() { public void logPointcut() {
@@ -80,8 +81,12 @@ public class LogAspect {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Long userId = null; Long userId = null;
// 获取所有匿名标记
Set<String> anonymousUrls = AnonymousUtils.getAnonymousUrls(applicationContext);
// 非登录请求获取用户ID登录请求在登录成功后(joinPoint.proceed())获取用户ID // 非登录请求获取用户ID登录请求在登录成功后(joinPoint.proceed())获取用户ID
if (!SecurityConstants.LOGIN_PATH.equals(requestURI)) { if (!anonymousUrls.contains(requestURI)) {
userId = SecurityUtils.getUserId(); userId = SecurityUtils.getUserId();
} }

View File

@@ -1,5 +1,6 @@
package com.youlai.boot.shared.auth.controller; package com.youlai.boot.shared.auth.controller;
import com.youlai.boot.common.annotation.methods.AnonymousPostMapping;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.shared.auth.model.RefreshTokenRequest; import com.youlai.boot.shared.auth.model.RefreshTokenRequest;
@@ -30,7 +31,7 @@ public class AuthController {
private final AuthService authService; private final AuthService authService;
@Operation(summary = "登录") @Operation(summary = "登录")
@PostMapping("/login") @AnonymousPostMapping("/login")
@Log(value = "登录", module = LogModuleEnum.LOGIN) @Log(value = "登录", module = LogModuleEnum.LOGIN)
public Result<AuthTokenResponse> login( public Result<AuthTokenResponse> login(
@Parameter(description = "用户名", example = "admin") @RequestParam String username, @Parameter(description = "用户名", example = "admin") @RequestParam String username,
@@ -63,7 +64,7 @@ public class AuthController {
} }
@Operation(summary = "微信登录") @Operation(summary = "微信登录")
@PostMapping("/wechat-login") @AnonymousPostMapping("/wechat-login")
@Log(value = "微信登录", module = LogModuleEnum.LOGIN) @Log(value = "微信登录", module = LogModuleEnum.LOGIN)
public Result<AuthTokenResponse> wechatLogin( public Result<AuthTokenResponse> wechatLogin(
@Parameter(description = "微信授权码", example = "code") @RequestParam String code @Parameter(description = "微信授权码", example = "code") @RequestParam String code