diff --git a/spring-security/ReadMe.md b/spring-security/ReadMe.md index f316720..80f6a75 100644 --- a/spring-security/ReadMe.md +++ b/spring-security/ReadMe.md @@ -1137,19 +1137,88 @@ public Result lowerUser(String name) { 在方法中写入自己的校验逻辑。 +`MethodInvocation`类型是执行方法之前相当于是一个反射,可以获取到当前方法上的注解、方法名称、当前类等。 + ```java -/** - * 处理方法调用后的授权检查 - * check()方法接收的是MethodInvocationResult对象,包含已执行方法的结果 - * 用于决定是否允许返回某个方法的结果(后置过滤) - * 这是Spring Security较新的"后置授权"功能 - */ @Component -public class PostAuthorizationManager implements AuthorizationManager { +@RequiredArgsConstructor +public class PreAuthorizationManager implements AuthorizationManager { + + private final SecurityConfigProperties securityConfigProperties; @Override - public AuthorizationDecision check(Supplier authentication, MethodInvocationResult invocation) { - return new AuthorizationDecision(true); + public AuthorizationDecision check(Supplier authenticationSupplier, MethodInvocation methodInvocation) { + Authentication authentication = authenticationSupplier.get(); + + // 如果方法有 @PreAuthorize 注解,会先到这里 + if (authentication == null || !authentication.isAuthenticated()) { + return new AuthorizationDecision(false); + } + + // 检查权限 + boolean granted = hasPermission(authentication, methodInvocation); + return new AuthorizationDecision(granted); + } + + private boolean hasPermission(Authentication authentication, MethodInvocation methodInvocation) { + PreAuthorize preAuthorize = AnnotationUtils.findAnnotation(methodInvocation.getMethod(), PreAuthorize.class); + if (preAuthorize == null) { + return true; // 没有注解默认放行 + } + + String expression = preAuthorize.value(); + // 解析表达式中的权限要求 + List requiredAuthorities = extractAuthoritiesFromExpression(expression); + + // 获取配置的admin权限 + List adminAuthorities = securityConfigProperties.getAdminAuthorities(); + + return authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .anyMatch(auth -> + adminAuthorities.contains(auth) || + requiredAuthorities.contains(auth) + ); + } + + private List extractAuthoritiesFromExpression(String expression) { + List authorities = new ArrayList<>(); + + // 处理 hasAuthority('permission') 格式 + Pattern hasAuthorityPattern = Pattern.compile("hasAuthority\\('([^']+)'\\)"); + Matcher hasAuthorityMatcher = hasAuthorityPattern.matcher(expression); + while (hasAuthorityMatcher.find()) { + authorities.add(hasAuthorityMatcher.group(1)); + } + + // 处理 hasRole('ROLE_XXX') 格式 (Spring Security 会自动添加 ROLE_ 前缀) + Pattern hasRolePattern = Pattern.compile("hasRole\\('([^']+)'\\)"); + Matcher hasRoleMatcher = hasRolePattern.matcher(expression); + while (hasRoleMatcher.find()) { + authorities.add(hasRoleMatcher.group(1)); + } + + // 处理 hasAnyAuthority('perm1','perm2') 格式 + Pattern hasAnyAuthorityPattern = Pattern.compile("hasAnyAuthority\\(([^)]+)\\)"); + Matcher hasAnyAuthorityMatcher = hasAnyAuthorityPattern.matcher(expression); + while (hasAnyAuthorityMatcher.find()) { + String[] perms = hasAnyAuthorityMatcher.group(1).split(","); + for (String perm : perms) { + authorities.add(perm.trim().replaceAll("'", "")); + } + } + + // 处理 hasAnyRole('role1','role2') 格式 + Pattern hasAnyRolePattern = Pattern.compile("hasAnyRole\\(([^)]+)\\)"); + Matcher hasAnyRoleMatcher = hasAnyRolePattern.matcher(expression); + while (hasAnyRoleMatcher.find()) { + String[] roles = hasAnyRoleMatcher.group(1).split(","); + for (String role : roles) { + authorities.add(role.trim().replaceAll("'", "")); + } + } + + return authorities; } } ``` @@ -1158,24 +1227,64 @@ public class PostAuthorizationManager implements AuthorizationManager { +@RequiredArgsConstructor +public class PostAuthorizationManager implements AuthorizationManager { + + private final SecurityConfigProperties securityConfigProperties; @Override - public AuthorizationDecision check(Supplier authentication, MethodInvocation invocation) { - return new AuthorizationDecision(true); + public AuthorizationDecision check(Supplier authenticationSupplier, MethodInvocationResult methodInvocationResult) { + Authentication authentication = authenticationSupplier.get(); + + // 如果方法有 @PreAuthorize 注解,会先到这里 + if (authentication == null || !authentication.isAuthenticated()) { + return new AuthorizationDecision(false); + } + + // 检查权限 + boolean granted = hasPermission(authentication, methodInvocationResult); + return new AuthorizationDecision(granted); } + private boolean hasPermission(Authentication authentication, MethodInvocationResult methodInvocationResult) { + // 获取当前校验方法的返回值 + if (methodInvocationResult.getResult() instanceof Result result) { + // 拿到当前返回值中权限内容 + List auths = result.getAuths(); + + // 允许全局访问的 角色或权限 + List adminAuthorities = securityConfigProperties.adminAuthorities; + + // 判断返回值中返回方法全新啊是否和用户权限匹配 + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority) + .anyMatch(auth -> + // 允许放行的角色或权限 和 匹配到的角色或权限 + adminAuthorities.contains(auth) || auths.contains(auth) + ); + } + + // ❗这里可以设置自己的返回状态 + // ====================================== + // 默认返回 TRUE 是因为有可能当前方法不需要验证 + // 所以才设置默认返回为 TURE + // ====================================== + return true; + } } ``` #### 3. 禁用自带的 +> [!IMPORTANT] +> +> 这是一个非常关键的一步,如果需要实现自定义,必须要禁用原来的,替换之前的;否则不会生效。 + 需要加上注解`@EnableMethodSecurity(prePostEnabled = false)`。 ```java @@ -1535,3 +1644,371 @@ Bunny [permission::read, role::read] ``` +### 创建过滤器 + +```java +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenService jwtTokenService; + private final DbUserDetailService userDetailsService; + private final SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint; + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) + throws ServletException, IOException { + try { + // 检查白名单路径 + if (isNoAuthPath(request)) { + filterChain.doFilter(request, response); + return; + } + + // 检查是否需要认证的路径 + if (!isSecurePath(request)) { + filterChain.doFilter(request, response); + return; + } + + // 验证 Token + if (validToken(request, response, filterChain)) return; + + filterChain.doFilter(request, response); + } catch (AuthenticSecurityException e) { + // 直接处理认证异常,不再调用filterChain.doFilter() + securityAuthenticationEntryPoint.commence( + request, + response, + new MyAuthenticationException(e.getMessage(), e) + ); + } catch (RuntimeException e) { + securityAuthenticationEntryPoint.commence( + request, + response, + new MyAuthenticationException("Authentication failed", e) + ); + } + } + + private boolean validToken(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { + // 验证Token + String authHeader = request.getHeader("Authorization"); + + // Token验证 + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return true; + // throw new AuthenticSecurityException(ResultCodeEnum.LOGIN_AUTH); + } + + String jwtToken = authHeader.substring(7); + + if (jwtTokenService.isExpired(jwtToken)) { + throw new AuthenticSecurityException(ResultCodeEnum.AUTHENTICATION_EXPIRED); + } + + // 设置认证信息 + String username = jwtTokenService.getUsernameFromToken(jwtToken); + Long userId = jwtTokenService.getUserIdFromToken(jwtToken); + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + BaseContext.setUsername(username); + BaseContext.setUserId(userId); + } + return false; + } + + /** + * 是否是不用验证的路径 + */ + private boolean isNoAuthPath(HttpServletRequest request) { + RequestMatcher[] matchers = SecurityWebConfiguration.noAuthPaths.stream() + .map(AntPathRequestMatcher::new) + .toArray(RequestMatcher[]::new); + return new OrRequestMatcher(matchers).matches(request); + } + + /** + * 是否是要验证的路径 + */ + private boolean isSecurePath(HttpServletRequest request) { + RequestMatcher[] matchers = SecurityWebConfiguration.securedPaths.stream() + .map(AntPathRequestMatcher::new) + .toArray(RequestMatcher[]::new); + return new OrRequestMatcher(matchers).matches(request); + } +} +``` + +### 添加过滤器 + +添加过滤器为之前认证。 + +```java +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true) +@RequiredArgsConstructor +public class SecurityWebConfiguration { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + + http + // ... + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + ; + + return http.build(); + } +} +``` + +## 常见问题 + +### 1. 返回两次 + +如果请求接口时候发现接口返回两次问题,往往是过滤器链写的有问题,比如使用过滤器链后面没有return。 + +比如下面写法就会导致返回两次情况。 + +#### 问题复现 + +```java +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) + throws ServletException, IOException { + try { + // 检查白名单路径 + // 略. + + // 检查是否需要认证的路径 + // 略. + + // ⚠⚠⚠ 这里没有 return + checkToken(request, response, filterChain); + + filterChain.doFilter(request, response); + } catch (AuthenticSecurityException e) { + // ... + } + } + + private void checkToken(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { + // 验证Token + // 设置认证信息,但是没有返回 ... + } + } + + /** + * 是否是不用验证的路径 + */ + private boolean isNoAuthPath(HttpServletRequest request) { + // ... + } + + /** + * 是否是要验证的路径 + */ + private boolean isSecurePath(HttpServletRequest request) { + // ... + } +} +``` + +#### 解决方案 + +z只需要将`checkToken`返回Boolean值,之后判断再return。 + +```java +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(@NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + @NotNull FilterChain filterChain) + throws ServletException, IOException { + try { + // 检查白名单路径 + // 略. + + // 检查是否需要认证的路径 + // 略. + + // ⚠⚠⚠ 这里判断是否为ture + if (checkToken(request, response, filterChain)) return; + + filterChain.doFilter(request, response); + } catch (AuthenticSecurityException e) { + // ... + } + } + + private boolean checkToken(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { + // 验证Token + // 设置认证信息,但是没有返回 ... + return ture Or false; + } + } + + /** + * 是否是不用验证的路径 + */ + private boolean isNoAuthPath(HttpServletRequest request) { + // ... + } + + /** + * 是否是要验证的路径 + */ + private boolean isSecurePath(HttpServletRequest request) { + // ... + } +} +``` + +### 2. requestMatchers和@PermitAll权重 + +> [!NOTE] +> +> 可参考文档:[在类或接口级别声明注解](https://docs.spring.io/spring-security/reference/6.3/servlet/authorization/method-security.html#class-or-interface-annotations) +> +> 官方文档中也提到方法授权和请求授权比较:[请求级授权与方法级授权的比较](https://docs.spring.io/spring-security/reference/6.3/servlet/authorization/method-security.html#request-vs-method) + +假如你在requestMatchers上配置了`/api/**`(只要是此接口下都要认证登录),之后你在`/api/abc`上加上了注解`@PermitAll`或者`@PreAuthorize("permitAll()")`,那么接口`/api/abc`并不会按照你所期望的结果运行(不用登录也可访问)。 + +因为Security会先校验你的requestMatchers,之后再去方法上验证。大多数开发者直觉上认为,方法注解权重高于requestMatchers,实际上,会校验requestMatchers上的路径之后再去方法。 + +那么类似`permitAll`这种注解应该怎么用,比如你在控制器上加上了注解`@PreAuthorize("hasAuthority('USER')")`,方法上的`@PreAuthorize("hasAuthority('ADMIN')")`会覆盖掉类上的,同理,如果在方法上加上`@PermitAll`也会覆盖掉类上的。 + +在官方文档中,并没用直接阐述requestMatchers高于`@PermitAll`只是大多数人觉得,`@PermitAll`高于requestMatchers实际不然; + +```java +@RequestMapping("/api/security/programmatically") +@PreAuthorize("hasAuthority('USER')") +public class SecurityProgrammaticallyController { + + @PreAuthorize("hasAuthority('ADMIN')") + @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") + @GetMapping("upper-user") + public Result upperUser() { + String data = "是区分大小写的"; + return Result.success(data); + } + +} +``` + +官方文档只提到了方法注解会覆盖掉类上注解,同时不可以在方法上加上多个注解比如像下面这样。 + +```java +@RequestMapping("/api/security/programmatically") +@PreAuthorize("hasAuthority('USER')") +public class SecurityProgrammaticallyController { + + @PreAuthorize("hasAuthority('ADMIN')") + @PreAuthorize("hasAuthority('USER')") + @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") + @GetMapping("upper-user") + public Result upperUser() { + String data = "是区分大小写的"; + return Result.success(data); + } + +} +``` + +如果要使用`@PermitAll`需要显式的开启。 + +```java +@EnableMethodSecurity(jsr250Enabled = true) // 启用了 JSR-250 +``` + +### 3. 忽略接口无法获取用户信息 + +> [!NOTE] +> +> 可以参考文档:[使用 SpEL 表达授权](https://docs.spring.io/spring-security/reference/6.3/servlet/authorization/method-security.html#authorization-expressions) + +假如说现在需求是`/api/**`的接口路径是都需要认证的,但是我们想忽略掉部分路径,比如:`/api/a/**`、`/api/b`,那么访问忽略的接口是无法获取到当前用户信息的。因为是不走校验的。 + +SpringSecurity官方解释的是,这样校验会更快。 + +```java +permitAll - The method requires no authorization to be invoked; note that in this case, the Authentication is never retrieved from the session + +denyAll - The method is not allowed under any circumstances; note that in this case, the Authentication is never retrieved from the session +``` + +### 4. 同时使用@PreAuthorize和@PermitAll达不到预期 + +- 如果当前用户无权限或者未登录: + + - 访问当前控制器必须要有`USER`权限,但是访问`upperUser`方法时无需登录或认证,这时`@PermitAll`不会起到作用,相反会拒绝请求。 + + - 即使显式的开启了jsr250Enabled也无效。 + + ```java + @EnableMethodSecurity(jsr250Enabled = true) // 启用了 JSR-250 + ``` + +```java +@RestController +@RequestMapping("/api/security/programmatically") +@PreAuthorize("hasAuthority('USER')") +public class SecurityProgrammaticallyController { + + @PermitAll + @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") + @GetMapping("upper-user") + public Result upperUser() { + String data = "是区分大小写的"; + return Result.success(data); + } + +} +``` + +解决方法是,使用`@PreAuthorize("permitAll()")` + +```java +@RestController +@RequestMapping("/api/security/programmatically") +@PreAuthorize("hasAuthority('USER')") +public class SecurityProgrammaticallyController { + + @PreAuthorize("permitAll()") + @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") + @GetMapping("upper-user") + public Result upperUser() { + String data = "是区分大小写的"; + return Result.success(data); + } + +} +``` + diff --git a/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java b/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java index 98df044..1981074 100644 --- a/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java +++ b/spring-security/step-2/src/main/java/com/spring/step2/controller/RoleController.java @@ -11,7 +11,6 @@ import com.spring.step2.service.RoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -48,7 +47,6 @@ public class RoleController { return Result.success(pageResult); } - @PermitAll @Operation(summary = "获取全部角色列表", description = "获取全部角色列表") @GetMapping("all") public Result> getRoleList() { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/PermissionController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/PermissionController.java index 36f8b2f..8e47611 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/PermissionController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/PermissionController.java @@ -12,7 +12,6 @@ import com.spring.step3.service.roles.PermissionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -35,7 +34,6 @@ public class PermissionController { private final PermissionService permissionService; - @PermitAll @Operation(summary = "分页查询系统权限表", description = "分页系统权限表") @GetMapping("{page}/{limit}") public Result> getPermissionPage( @@ -49,7 +47,6 @@ public class PermissionController { return Result.success(pageResult); } - @PermitAll @Operation(summary = "所有的权限列表", description = "获取所有的权限列表") @GetMapping("all") public Result> getAllPermission() { @@ -57,7 +54,6 @@ public class PermissionController { return Result.success(voList); } - @PermitAll @Operation(summary = "添加系统权限表", description = "添加系统权限表") @PostMapping() public Result addPermission(@Valid @RequestBody PermissionDto dto) { @@ -65,7 +61,6 @@ public class PermissionController { return Result.success(ResultCodeEnum.ADD_SUCCESS); } - @PermitAll @Operation(summary = "更新系统权限表", description = "更新系统权限表") @PutMapping() public Result updatePermission(@Valid @RequestBody PermissionDto dto) { @@ -73,7 +68,6 @@ public class PermissionController { return Result.success(ResultCodeEnum.UPDATE_SUCCESS); } - @PermitAll @Operation(summary = "删除系统权限表", description = "删除系统权限表") @DeleteMapping() public Result deletePermission(@RequestBody List ids) { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/RoleController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/RoleController.java index 08ad618..a0110be 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/RoleController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/RoleController.java @@ -11,7 +11,6 @@ import com.spring.step3.service.roles.RoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -35,7 +34,6 @@ public class RoleController { private final RoleService roleService; - @PermitAll @Operation(summary = "分页查询系统角色表", description = "分页系统角色表") @GetMapping("{page}/{limit}") public Result> getRolePage( @@ -49,7 +47,6 @@ public class RoleController { return Result.success(pageResult); } - @PermitAll @Operation(summary = "获取全部角色列表", description = "获取全部角色列表") @GetMapping("all") public Result> getRoleList() { @@ -57,7 +54,6 @@ public class RoleController { return Result.success(roleVoList); } - @PermitAll @Operation(summary = "添加系统角色表", description = "添加系统角色表") @PostMapping() public Result addRole(@Valid @RequestBody RoleDto dto) { @@ -65,7 +61,6 @@ public class RoleController { return Result.success(ResultCodeEnum.ADD_SUCCESS); } - @PermitAll @Operation(summary = "更新系统角色表", description = "更新系统角色表") @PutMapping() public Result updateRole(@Valid @RequestBody RoleDto dto) { @@ -73,7 +68,6 @@ public class RoleController { return Result.success(ResultCodeEnum.UPDATE_SUCCESS); } - @PermitAll @Operation(summary = "删除系统角色表", description = "删除系统角色表") @DeleteMapping() public Result deleteRole(@RequestBody List ids) { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/RolePermissionController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/RolePermissionController.java index 7c3cf04..e5b1fa5 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/RolePermissionController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/RolePermissionController.java @@ -13,7 +13,6 @@ import com.spring.step3.service.roles.RolePermissionService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -36,7 +35,6 @@ public class RolePermissionController { private final RolePermissionService rolePermissionService; - @PermitAll @Operation(summary = "分页查询角色权限关联表", description = "分页角色权限关联表") @GetMapping("{page}/{limit}") public Result> getRolePermissionPage( @@ -51,14 +49,11 @@ public class RolePermissionController { } @GetMapping("permissions") - @PermitAll - @Operation(summary = "根据角色id获取权限内容", description = "根据角色id获取权限内容") public Result> getRolePermissionById(Long permissionId) { List voList = rolePermissionService.getRolePermissionById(permissionId); return Result.success(voList); } - @PermitAll @Operation(summary = "添加角色权限关联表", description = "添加角色权限关联表") @PostMapping() public Result addRolePermission(@Valid @RequestBody RolePermissionDto dto) { @@ -66,7 +61,6 @@ public class RolePermissionController { return Result.success(ResultCodeEnum.ADD_SUCCESS); } - @PermitAll @Operation(summary = "为角色分配权限", description = "根据角色id分配权限") @PostMapping("assign-permission") public Result assignRolePermission(@Valid @RequestBody AssignRolePermissionDto dto) { @@ -74,7 +68,6 @@ public class RolePermissionController { return Result.success(); } - @PermitAll @Operation(summary = "更新角色权限关联表", description = "更新角色权限关联表") @PutMapping() public Result updateRolePermission(@Valid @RequestBody RolePermissionDto dto) { @@ -82,7 +75,6 @@ public class RolePermissionController { return Result.success(ResultCodeEnum.UPDATE_SUCCESS); } - @PermitAll @Operation(summary = "删除角色权限关联表", description = "删除角色权限关联表") @DeleteMapping() public Result deleteRolePermission(@RequestBody List ids) { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/UserController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/UserController.java index 673a76e..4df6a2a 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/UserController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/UserController.java @@ -27,6 +27,7 @@ import java.util.List; * @since 2025-07-11 22:36:53 */ @Tag(name = "用户基本信息表", description = "用户基本信息表相关接口") +@PermitAll @RestController @RequestMapping("/api/user") @RequiredArgsConstructor @@ -48,7 +49,6 @@ public class UserController { return Result.success(pageResult, ResultCodeEnum.LOAD_FINISHED); } - @PermitAll @Operation(summary = "添加用户基本信息表", description = "添加用户基本信息表") @PostMapping() public Result addUser(@Valid @RequestBody UserDto dto) { @@ -56,7 +56,6 @@ public class UserController { return Result.success(ResultCodeEnum.ADD_SUCCESS); } - @PermitAll @Operation(summary = "更新用户基本信息表", description = "更新用户基本信息表") @PutMapping() public Result updateUser(@Valid @RequestBody UserDto dto) { @@ -64,7 +63,6 @@ public class UserController { return Result.success(ResultCodeEnum.UPDATE_SUCCESS); } - @PermitAll @Operation(summary = "删除用户基本信息表", description = "删除用户基本信息表") @DeleteMapping() public Result deleteUser(@RequestBody List ids) { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/UserRoleController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/UserRoleController.java index e7e8582..dff9e71 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/UserRoleController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/UserRoleController.java @@ -12,7 +12,6 @@ import com.spring.step3.service.user.UserRoleService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.annotation.security.PermitAll; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -35,7 +34,6 @@ public class UserRoleController { private final UserRoleService userRoleService; - @PermitAll @Operation(summary = "分页查询用户角色关联表", description = "分页用户角色关联表") @GetMapping("{page}/{limit}") public Result> getUserRolePage( @@ -49,7 +47,6 @@ public class UserRoleController { return Result.success(pageResult); } - @PermitAll @Operation(summary = "根据用户id获取当前用户角色列表", description = "根据用户id获取当前用户角色列表") @GetMapping("roles") public Result> getRoleListByUserId(Long userId) { @@ -57,7 +54,6 @@ public class UserRoleController { return Result.success(voList); } - @PermitAll @Operation(summary = "添加用户角色关联表", description = "添加用户角色关联表") @PostMapping() public Result addUserRole(@Valid @RequestBody UserRoleDto dto) { @@ -65,7 +61,6 @@ public class UserRoleController { return Result.success(ResultCodeEnum.ADD_SUCCESS); } - @PermitAll @Operation(summary = "为用户分配角色id", description = "根据用户id分配用户角色") @PostMapping("assign-role") public Result assignUserRole(@Valid @RequestBody AssignUserRoleDto dto) { @@ -73,7 +68,6 @@ public class UserRoleController { return Result.success(); } - @PermitAll @Operation(summary = "更新用户角色关联表", description = "更新用户角色关联表") @PutMapping() public Result updateUserRole(@Valid @RequestBody UserRoleDto dto) { @@ -81,7 +75,6 @@ public class UserRoleController { return Result.success(ResultCodeEnum.UPDATE_SUCCESS); } - @PermitAll @Operation(summary = "删除用户角色关联表", description = "删除用户角色关联表") @DeleteMapping() public Result deleteUserRole(@RequestBody List ids) { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityPreAuthorizationController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityPreAuthorizationController.java index 030f891..f8615ed 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityPreAuthorizationController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityPreAuthorizationController.java @@ -3,19 +3,17 @@ package com.spring.step3.controller.test; import com.spring.step3.domain.vo.result.Result; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @Tag(name = "自定义方法前的校验", description = "自定义方法前的校验 SecurityPreAuthorization") -@Slf4j @RestController @RequestMapping("/api/security/pre") public class SecurityPreAuthorizationController { - @PreAuthorize("hasAuthority('role::read')") + @PreAuthorize("hasPermission('role::read')") @Operation(summary = "拥有 role:read 的角色可以访问", description = "当前用户拥有 role:read 角色可以访问这个接口") @GetMapping("role-user") public Result roleUser() { @@ -30,7 +28,7 @@ public class SecurityPreAuthorizationController { return Result.success(data); } - @PreAuthorize("hasAuthority('admin')") + @PreAuthorize("'admin'") @Operation(summary = "拥有 admin 的角色可以访问", description = "当前用户拥有 admin 角色可以访问这个接口") @GetMapping("lower-admin") public Result lowerAdmin() { diff --git a/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityProgrammaticallyController.java b/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityProgrammaticallyController.java index 5ff582b..3171ed9 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityProgrammaticallyController.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/controller/test/SecurityProgrammaticallyController.java @@ -3,6 +3,7 @@ package com.spring.step3.controller.test; import com.spring.step3.domain.vo.result.Result; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.security.PermitAll; import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; @@ -13,8 +14,11 @@ import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequestMapping("/api/security/programmatically") +@PreAuthorize("hasAuthority('USER')") public class SecurityProgrammaticallyController { + // @PreAuthorize("permitAll()") + @PermitAll @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") @GetMapping("upper-user") public Result upperUser() { @@ -23,7 +27,7 @@ public class SecurityProgrammaticallyController { } @PreAuthorize("@auth.decide(#name)") - @Operation(summary = "拥有 USER 的角色可以访问", description = "当前用户拥有 USER 角色可以访问这个接口") + @Operation(summary = "auth.decide访问", description = "auth.decide访问") @GetMapping("lower-user") public Result lowerUser(String name) { return Result.success(name); diff --git a/spring-security/step-3/src/main/java/com/spring/step3/domain/dto/AuthLogDto.java b/spring-security/step-3/src/main/java/com/spring/step3/domain/dto/AuthLogDto.java new file mode 100644 index 0000000..df2fba9 --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/domain/dto/AuthLogDto.java @@ -0,0 +1,52 @@ +package com.spring.step3.domain.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(name = "AuthLogDTO对象", title = "系统授权日志表", description = "系统授权日志表的DTO对象") +public class AuthLogDto { + + @Schema(name = "eventType", title = "事件类型(GRANTED=授权成功,DENIED=授权拒绝)") + private String eventType; + + @Schema(name = "username", title = "用户名") + private String username; + + @Schema(name = "userId", title = "用户ID") + private Long userId; + + @Schema(name = "requestIp", title = "请求IP") + private String requestIp; + + @Schema(name = "requestMethod", title = "请求方法(GET,POST等)") + private String requestMethod; + + @Schema(name = "requestUri", title = "请求URI") + private String requestUri; + + @Schema(name = "className", title = "类名") + private String className; + + @Schema(name = "methodName", title = "方法名") + private String methodName; + + @Schema(name = "methodParams", title = "方法参数(JSON格式)") + private String methodParams; + + @Schema(name = "requiredAuthority", title = "所需权限表达式") + private String requiredAuthority; + + @Schema(name = "userAuthorities", title = "用户拥有的权限(JSON格式)") + private String userAuthorities; + + @Schema(name = "decisionReason", title = "决策原因") + private String decisionReason; + + @Schema(name = "exceptionMessage", title = "异常信息") + private String exceptionMessage; + + @Schema(name = "isDeleted", title = "删除标志(0=未删除 1=已删除)") + private Boolean isDeleted; + +} \ No newline at end of file diff --git a/spring-security/step-3/src/main/java/com/spring/step3/domain/entity/AuthLogEntity.java b/spring-security/step-3/src/main/java/com/spring/step3/domain/entity/AuthLogEntity.java new file mode 100644 index 0000000..f2f6020 --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/domain/entity/AuthLogEntity.java @@ -0,0 +1,59 @@ +package com.spring.step3.domain.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import com.spring.step3.domain.entity.base.BaseEntity; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Getter +@Setter +@Accessors(chain = true) +@TableName("sys_auth_log") +@Schema(name = "AuthLog对象", title = "系统授权日志表", description = "系统授权日志表的实体类对象") +public class AuthLogEntity extends BaseEntity { + + @Schema(name = "eventType", title = "事件类型(GRANTED=授权成功,DENIED=授权拒绝)") + private String eventType; + + @Schema(name = "username", title = "用户名") + private String username; + + @Schema(name = "userId", title = "用户ID") + private Long userId; + + @Schema(name = "requestIp", title = "请求IP") + private String requestIp; + + @Schema(name = "requestMethod", title = "请求方法(GET,POST等)") + private String requestMethod; + + @Schema(name = "requestUri", title = "请求URI") + private String requestUri; + + @Schema(name = "className", title = "类名") + private String className; + + @Schema(name = "methodName", title = "方法名") + private String methodName; + + @Schema(name = "methodParams", title = "方法参数(JSON格式)") + private String methodParams; + + @Schema(name = "requiredAuthority", title = "所需权限表达式") + private String requiredAuthority; + + @Schema(name = "userAuthorities", title = "用户拥有的权限(JSON格式)") + private String userAuthorities; + + @Schema(name = "decisionReason", title = "决策原因") + private String decisionReason; + + @Schema(name = "exceptionMessage", title = "异常信息") + private String exceptionMessage; + + @Schema(name = "isDeleted", title = "删除标志(0=未删除 1=已删除)") + private Boolean isDeleted; + +} \ No newline at end of file diff --git a/spring-security/step-3/src/main/java/com/spring/step3/domain/vo/AuthLogVo.java b/spring-security/step-3/src/main/java/com/spring/step3/domain/vo/AuthLogVo.java new file mode 100644 index 0000000..ee3a811 --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/domain/vo/AuthLogVo.java @@ -0,0 +1,60 @@ +package com.spring.step3.domain.vo; + +import com.spring.step3.domain.vo.base.BaseVo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +@Schema(name = "AuthLogVO对象", title = "系统授权日志表", description = "系统授权日志表的VO对象") +public class AuthLogVo extends BaseVo { + + @Schema(name = "eventType", title = "事件类型(GRANTED=授权成功,DENIED=授权拒绝)") + private String eventType; + + @Schema(name = "username", title = "用户名") + private String username; + + @Schema(name = "userId", title = "用户ID") + private Long userId; + + @Schema(name = "requestIp", title = "请求IP") + private String requestIp; + + @Schema(name = "requestMethod", title = "请求方法(GET,POST等)") + private String requestMethod; + + @Schema(name = "requestUri", title = "请求URI") + private String requestUri; + + @Schema(name = "className", title = "类名") + private String className; + + @Schema(name = "methodName", title = "方法名") + private String methodName; + + @Schema(name = "methodParams", title = "方法参数(JSON格式)") + private String methodParams; + + @Schema(name = "requiredAuthority", title = "所需权限表达式") + private String requiredAuthority; + + @Schema(name = "userAuthorities", title = "用户拥有的权限(JSON格式)") + private String userAuthorities; + + @Schema(name = "decisionReason", title = "决策原因") + private String decisionReason; + + @Schema(name = "exceptionMessage", title = "异常信息") + private String exceptionMessage; + + @Schema(name = "isDeleted", title = "删除标志(0=未删除 1=已删除)") + private Boolean isDeleted; + +} + diff --git a/spring-security/step-3/src/main/java/com/spring/step3/mapper/AuthLogMapper.java b/spring-security/step-3/src/main/java/com/spring/step3/mapper/AuthLogMapper.java new file mode 100644 index 0000000..4bbe59b --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/mapper/AuthLogMapper.java @@ -0,0 +1,33 @@ +package com.spring.step3.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.spring.step3.domain.dto.AuthLogDto; +import com.spring.step3.domain.entity.AuthLogEntity; +import com.spring.step3.domain.vo.AuthLogVo; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + *

+ * 系统授权日志表 Mapper 接口 + *

+ * + * @author AuthoritySystem + * @since 2025-07-19 14:26:58 + */ +@Mapper +public interface AuthLogMapper extends BaseMapper { + + /** + * 分页查询系统授权日志表内容 + * + * @param pageParams 系统授权日志表分页参数 + * @param dto 系统授权日志表查询表单 + * @return 系统授权日志表分页结果 + */ + IPage selectListByPage(@Param("page") Page pageParams, @Param("dto") AuthLogDto dto); + +} diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/config/SecurityWebConfiguration.java b/spring-security/step-3/src/main/java/com/spring/step3/security/config/SecurityWebConfiguration.java index 93548ea..429809d 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/config/SecurityWebConfiguration.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/config/SecurityWebConfiguration.java @@ -3,6 +3,7 @@ package com.spring.step3.security.config; import com.spring.step3.security.filter.JwtAuthenticationFilter; import com.spring.step3.security.handler.SecurityAccessDeniedHandler; import com.spring.step3.security.handler.SecurityAuthenticationEntryPoint; +import com.spring.step3.security.properties.SecurityConfigProperties; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -14,24 +15,21 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import java.util.List; - @Configuration @EnableWebSecurity -@EnableMethodSecurity(jsr250Enabled = true, securedEnabled = true) +@EnableMethodSecurity(jsr250Enabled = true) @RequiredArgsConstructor public class SecurityWebConfiguration { - public static List securedPaths = List.of("/api/**"); - public static List noAuthPaths = List.of("/*/login"); private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final SecurityConfigProperties pathsProperties; @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 前端段分离不需要---禁用明文验证 - .httpBasic(AbstractHttpConfigurer::disable) + // .httpBasic(AbstractHttpConfigurer::disable) // 前端段分离不需要---禁用默认登录页 .formLogin(AbstractHttpConfigurer::disable) // 前端段分离不需要---禁用退出页 @@ -45,18 +43,19 @@ public class SecurityWebConfiguration { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) + // 如果要对部分接口做登录校验 或者 项目中需要使用粗粒度的 校验 .authorizeHttpRequests(authorizeRequests -> // 访问路径为 /api 时需要进行认证 authorizeRequests - // 不认证登录接口 - .requestMatchers(noAuthPaths.toArray(String[]::new)).permitAll() - // ❗只认证 securedPaths 下的所有接口 - // ======================================================================= - // 也可以在这里写多参数传入,如:"/api/**","/admin/**" - // 但是在 Spring过滤器中,如果要放行不需要认证请求,但是需要认证的接口必需要携带token。 - // 做法是在这里定义要认证的接口,如果要做成动态可以放到数据库。 - // ======================================================================= - // .requestMatchers(securedPaths.toArray(String[]::new)).authenticated() + // // 不认证登录接口 + // .requestMatchers(pathsProperties.noAuthPaths.toArray(String[]::new)).permitAll() + // // ❗只认证 securedPaths 下的所有接口 + // // ======================================================================= + // // 也可以在这里写多参数传入,如:"/api/**","/admin/**" + // // 但是在 Spring过滤器中,如果要放行不需要认证请求,但是需要认证的接口必需要携带token。 + // // 做法是在这里定义要认证的接口,如果要做成动态可以放到数据库。 + // // ======================================================================= + // .requestMatchers(pathsProperties.securedPaths.toArray(String[]::new)).authenticated() // 其余请求都放行 .anyRequest().permitAll() ) @@ -66,7 +65,7 @@ public class SecurityWebConfiguration { // 没有权限访问 exception.accessDeniedHandler(new SecurityAccessDeniedHandler()); }) - .addFilterAfter(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) ; return http.build(); diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/event/AuthenticationEvents.java b/spring-security/step-3/src/main/java/com/spring/step3/security/event/AuthenticationEvents.java index ca78e3e..7382d7a 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/event/AuthenticationEvents.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/event/AuthenticationEvents.java @@ -1,5 +1,11 @@ package com.spring.step3.security.event; +import com.alibaba.fastjson2.JSON; +import com.spring.step3.config.context.BaseContext; +import com.spring.step3.domain.entity.AuthLogEntity; +import com.spring.step3.service.log.AuthLogService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.aopalliance.intercept.MethodInvocation; import org.springframework.context.event.EventListener; @@ -8,14 +14,18 @@ import org.springframework.security.authorization.event.AuthorizationDeniedEvent import org.springframework.security.authorization.event.AuthorizationGrantedEvent; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import java.lang.reflect.Method; -import java.util.Arrays; @Slf4j @Component +@RequiredArgsConstructor public class AuthenticationEvents { + private final AuthLogService authLogService; + /** * 监听拒绝授权内容 * @@ -24,29 +34,48 @@ public class AuthenticationEvents { @EventListener public void onFailure(AuthorizationDeniedEvent failure) { try { - // getSource 和 getObject意思一样,一种是传入泛型自动转换一种是要手动转换 - Object source = failure.getSource(); - - // 直接获取泛型对象 + // 当前执行的方法 MethodInvocation methodInvocation = failure.getObject(); + // 方法名称 Method method = methodInvocation.getMethod(); + // 方法参数 Object[] args = methodInvocation.getArguments(); - log.warn("方法调用被拒绝: {}.{}, 参数: {}", - method.getDeclaringClass().getSimpleName(), - method.getName(), - Arrays.toString(args)); - - // 这里面的信息,和接口 /api/security/current-user 内容一样 + // 用户身份 Authentication authentication = failure.getAuthentication().get(); + // 用户名 + String username = authentication.getName(); + // 决策结果 + AuthorizationDecision decision = failure.getAuthorizationDecision(); - AuthorizationDecision authorizationDecision = failure.getAuthorizationDecision(); - // ExpressionAuthorizationDecision [granted=false, expressionAttribute=hasAuthority('ADMIN')] - System.out.println(authorizationDecision); + // 获取请求上下文信息 + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + + AuthLogEntity authLog = new AuthLogEntity(); + if (attributes != null) { + HttpServletRequest request = attributes.getRequest(); + authLog.setRequestIp(request.getRemoteAddr()); + authLog.setRequestMethod(request.getMethod()); + authLog.setRequestUri(request.getRequestURI()); + } + + // 构建日志实体 + authLog.setEventType("DENIED"); + authLog.setUsername(username); + // 需要实现获取用户ID的方法 + authLog.setUserId(BaseContext.getUserId()); + authLog.setClassName(method.getDeclaringClass().getName()); + authLog.setMethodName(method.getName()); + authLog.setMethodParams(JSON.toJSONString(args)); + authLog.setRequiredAuthority(decision.toString()); + authLog.setUserAuthorities(JSON.toJSONString(authentication.getAuthorities())); + authLog.setCreateUser(BaseContext.getUserId()); + + // 保存到数据库 + authLogService.save(authLog); - log.warn("授权失败 - 用户: {}, 权限: {}", authentication.getName(), authorizationDecision); } catch (Exception e) { - log.info(e.getMessage()); + log.error("记录授权失败日志异常", e); } } diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/filter/JwtAuthenticationFilter.java b/spring-security/step-3/src/main/java/com/spring/step3/security/filter/JwtAuthenticationFilter.java index 0c2d3b7..8616aac 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/filter/JwtAuthenticationFilter.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/filter/JwtAuthenticationFilter.java @@ -4,8 +4,8 @@ import com.spring.step3.config.context.BaseContext; import com.spring.step3.domain.vo.result.ResultCodeEnum; import com.spring.step3.exception.AuthenticSecurityException; import com.spring.step3.exception.MyAuthenticationException; -import com.spring.step3.security.config.SecurityWebConfiguration; import com.spring.step3.security.handler.SecurityAuthenticationEntryPoint; +import com.spring.step3.security.properties.SecurityConfigProperties; import com.spring.step3.security.service.DbUserDetailService; import com.spring.step3.security.service.JwtTokenService; import jakarta.servlet.FilterChain; @@ -35,6 +35,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenService jwtTokenService; private final DbUserDetailService userDetailsService; private final SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint; + private final SecurityConfigProperties pathsProperties; @Override protected void doFilterInternal(@NotNull HttpServletRequest request, @@ -54,8 +55,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return; } - // 验证Token - validateAndSetAuthentication(request, response, filterChain); + // 验证 Token + if (validToken(request, response, filterChain)) { + filterChain.doFilter(request, response); + return; + } filterChain.doFilter(request, response); } catch (AuthenticSecurityException e) { @@ -74,36 +78,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { } } - /** - * 是否是不用验证的路径 - */ - private boolean isNoAuthPath(HttpServletRequest request) { - RequestMatcher[] matchers = SecurityWebConfiguration.noAuthPaths.stream() - .map(AntPathRequestMatcher::new) - .toArray(RequestMatcher[]::new); - return new OrRequestMatcher(matchers).matches(request); - } - - /** - * 是否是要验证的路径 - */ - private boolean isSecurePath(HttpServletRequest request) { - RequestMatcher[] matchers = SecurityWebConfiguration.securedPaths.stream() - .map(AntPathRequestMatcher::new) - .toArray(RequestMatcher[]::new); - return new OrRequestMatcher(matchers).matches(request); - } - - /** - * 验证并设置身份验证 - */ - private void validateAndSetAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + private boolean validToken(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws IOException, ServletException { + // 验证Token String authHeader = request.getHeader("Authorization"); // Token验证 if (authHeader == null || !authHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; + return true; // throw new AuthenticSecurityException(ResultCodeEnum.LOGIN_AUTH); } @@ -129,5 +110,26 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { BaseContext.setUsername(username); BaseContext.setUserId(userId); } + return false; + } + + /** + * 是否是不用验证的路径 + */ + private boolean isNoAuthPath(HttpServletRequest request) { + RequestMatcher[] matchers = pathsProperties.noAuthPaths.stream() + .map(AntPathRequestMatcher::new) + .toArray(RequestMatcher[]::new); + return new OrRequestMatcher(matchers).matches(request); + } + + /** + * 是否是要验证的路径 + */ + private boolean isSecurePath(HttpServletRequest request) { + RequestMatcher[] matchers = pathsProperties.securedPaths.stream() + .map(AntPathRequestMatcher::new) + .toArray(RequestMatcher[]::new); + return new OrRequestMatcher(matchers).matches(request); } } \ No newline at end of file diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/handler/SecurityAccessDeniedHandler.java b/spring-security/step-3/src/main/java/com/spring/step3/security/handler/SecurityAccessDeniedHandler.java index 02741fd..4bdbb8e 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/handler/SecurityAccessDeniedHandler.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/handler/SecurityAccessDeniedHandler.java @@ -20,7 +20,7 @@ public class SecurityAccessDeniedHandler implements AccessDeniedHandler { log.error("SecurityAccessDeniedHandler:{}", accessDeniedException.getLocalizedMessage()); // 无权访问接口 - Result result = Result.error(accessDeniedException.getMessage(), ResultCodeEnum.LOGIN_AUTH); + Result result = Result.error(accessDeniedException.getMessage(), ResultCodeEnum.FAIL_NO_ACCESS_DENIED); ResponseUtil.out(response, result); } } diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PostAuthorizationManager.java b/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PostAuthorizationManager.java index 640ac90..e10cff3 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PostAuthorizationManager.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PostAuthorizationManager.java @@ -1,6 +1,8 @@ package com.spring.step3.security.manger; import com.spring.step3.domain.vo.result.Result; +import com.spring.step3.security.properties.SecurityConfigProperties; +import lombok.RequiredArgsConstructor; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.method.MethodInvocationResult; @@ -18,40 +20,11 @@ import java.util.function.Supplier; * 这是Spring Security较新的"后置授权"功能 */ @Component +@RequiredArgsConstructor public class PostAuthorizationManager implements AuthorizationManager { - /** - * 这里两个实现方法按照Security官方要求进行实现 - *

类说明:

- * 下面的实现是对方法执行前进行权限校验的判断 - *
-     *     AuthorizationManager <MethodInvocation>
-     * 
- * 下面的这个是对方法执行后对权限的判断 - *
-     *     AuthorizationManager <MethodInvocationResult>
-     * 
- * - *

注意事项:

- * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。 - *
-     * @Configuration
-     * @EnableMethodSecurity(prePostEnabled = false)
-     * class MethodSecurityConfig {
-     *     @Bean
-     *     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-     *    Advisor preAuthorize(MyAuthorizationManager manager) {
-     * 		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
-     *    }
-     *
-     *    @Bean
-     *    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-     *    Advisor postAuthorize(MyAuthorizationManager manager) {
-     * 		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
-     *    }
-     * }
-     * 
- */ + private final SecurityConfigProperties securityConfigProperties; + @Override public AuthorizationDecision check(Supplier authenticationSupplier, MethodInvocationResult methodInvocationResult) { Authentication authentication = authenticationSupplier.get(); @@ -67,17 +40,19 @@ public class PostAuthorizationManager implements AuthorizationManager result) { // 拿到当前返回值中权限内容 List auths = result.getAuths(); + // 允许全局访问的 角色或权限 + List adminAuthorities = securityConfigProperties.adminAuthorities; + // 判断返回值中返回方法全新啊是否和用户权限匹配 return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority) .anyMatch(auth -> - // ❗这里是忽略了大小写匹配的 admin 权限,如果包含 admin 无论大小写都可以放行 - auth.equalsIgnoreCase("admin") - || auths.contains(auth) + // 允许放行的角色或权限 和 匹配到的角色或权限 + adminAuthorities.contains(auth) || auths.contains(auth) ); } diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PreAuthorizationManager.java b/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PreAuthorizationManager.java index a02224a..ea31bbe 100644 --- a/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PreAuthorizationManager.java +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/manger/PreAuthorizationManager.java @@ -1,14 +1,21 @@ package com.spring.step3.security.manger; +import com.spring.step3.security.properties.SecurityConfigProperties; +import lombok.RequiredArgsConstructor; import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; -import java.util.Collection; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * 处理方法调用前的授权检查 @@ -17,40 +24,11 @@ import java.util.function.Supplier; * 这是传统的"前置授权"模式 */ @Component +@RequiredArgsConstructor public class PreAuthorizationManager implements AuthorizationManager { - /** - * 这里两个实现方法按照Security官方要求进行实现 - *

类说明:

- * 下面的实现是对方法执行前进行权限校验的判断 - *
-     *     AuthorizationManager <MethodInvocation>
-     * 
- * 下面的这个是对方法执行后对权限的判断 - *
-     *     AuthorizationManager <MethodInvocationResult>
-     * 
- * - *

注意事项:

- * 将上述两个方法按照自定义的方式进行实现后,还需要禁用默认的。 - *
-     * @Configuration
-     * @EnableMethodSecurity(prePostEnabled = false)
-     * class MethodSecurityConfig {
-     *     @Bean
-     *     @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-     *    Advisor preAuthorize(MyAuthorizationManager manager) {
-     * 		return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager);
-     *    }
-     *
-     *    @Bean
-     *    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
-     *    Advisor postAuthorize(MyAuthorizationManager manager) {
-     * 		return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager);
-     *    }
-     * }
-     * 
- */ + private final SecurityConfigProperties securityConfigProperties; + @Override public AuthorizationDecision check(Supplier authenticationSupplier, MethodInvocation methodInvocation) { Authentication authentication = authenticationSupplier.get(); @@ -66,18 +44,63 @@ public class PreAuthorizationManager implements AuthorizationManager authorities = authentication.getAuthorities(); + String expression = preAuthorize.value(); + // 解析表达式中的权限要求 + List requiredAuthorities = extractAuthoritiesFromExpression(expression); - // 3. 实现你的权限逻辑 - // 这里简单示例:检查方法名是否包含在权限中 - String methodName = methodInvocation.getMethod().getName(); - return authorities.stream() + // 获取配置的admin权限 + List adminAuthorities = securityConfigProperties.getAdminAuthorities(); + + return authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) - // ❗这里是忽略了大小写匹配的 admin 权限,如果包含 admin 无论大小写都可以放行 - .anyMatch(auth -> auth.equalsIgnoreCase("admin") || auth.equals(methodName)); + .anyMatch(auth -> + adminAuthorities.contains(auth) || + requiredAuthorities.contains(auth) + ); + } + + private List extractAuthoritiesFromExpression(String expression) { + List authorities = new ArrayList<>(); + + // 处理 hasAuthority('permission') 格式 + Pattern hasAuthorityPattern = Pattern.compile("hasAuthority\\('([^']+)'\\)"); + Matcher hasAuthorityMatcher = hasAuthorityPattern.matcher(expression); + while (hasAuthorityMatcher.find()) { + authorities.add(hasAuthorityMatcher.group(1)); + } + + // 处理 hasRole('ROLE_XXX') 格式 (Spring Security 会自动添加 ROLE_ 前缀) + Pattern hasRolePattern = Pattern.compile("hasRole\\('([^']+)'\\)"); + Matcher hasRoleMatcher = hasRolePattern.matcher(expression); + while (hasRoleMatcher.find()) { + authorities.add(hasRoleMatcher.group(1)); + } + + // 处理 hasAnyAuthority('perm1','perm2') 格式 + Pattern hasAnyAuthorityPattern = Pattern.compile("hasAnyAuthority\\(([^)]+)\\)"); + Matcher hasAnyAuthorityMatcher = hasAnyAuthorityPattern.matcher(expression); + while (hasAnyAuthorityMatcher.find()) { + String[] perms = hasAnyAuthorityMatcher.group(1).split(","); + for (String perm : perms) { + authorities.add(perm.trim().replaceAll("'", "")); + } + } + + // 处理 hasAnyRole('role1','role2') 格式 + Pattern hasAnyRolePattern = Pattern.compile("hasAnyRole\\(([^)]+)\\)"); + Matcher hasAnyRoleMatcher = hasAnyRolePattern.matcher(expression); + while (hasAnyRoleMatcher.find()) { + String[] roles = hasAnyRoleMatcher.group(1).split(","); + for (String role : roles) { + authorities.add(role.trim().replaceAll("'", "")); + } + } + + return authorities; } } \ No newline at end of file diff --git a/spring-security/step-3/src/main/java/com/spring/step3/security/properties/SecurityConfigProperties.java b/spring-security/step-3/src/main/java/com/spring/step3/security/properties/SecurityConfigProperties.java new file mode 100644 index 0000000..75c42a8 --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/security/properties/SecurityConfigProperties.java @@ -0,0 +1,27 @@ +package com.spring.step3.security.properties; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "security-path") +@Schema(name = "SecurityPathsProperties对象", description = "路径忽略和认证") +public class SecurityConfigProperties { + + @Schema(name = "noAuthPaths", description = "不用认证的路径") + public List noAuthPaths; + + @Schema(name = "securedPaths", description = "需要认证的路径") + public List securedPaths; + + @Schema(name = "允许的角色或权限", description = "允许的角色或权限") + public List adminAuthorities; + +} diff --git a/spring-security/step-3/src/main/java/com/spring/step3/service/log/AuthLogService.java b/spring-security/step-3/src/main/java/com/spring/step3/service/log/AuthLogService.java new file mode 100644 index 0000000..1d9901d --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/service/log/AuthLogService.java @@ -0,0 +1,58 @@ +package com.spring.step3.service.log; + + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.spring.step3.domain.dto.AuthLogDto; +import com.spring.step3.domain.entity.AuthLogEntity; +import com.spring.step3.domain.vo.AuthLogVo; +import com.spring.step3.domain.vo.result.PageResult; + +import java.util.List; + +/** + *

+ * 系统授权日志表 服务类 + *

+ * + * @author Bunny + * @since 2025-07-19 14:26:58 + */ +public interface AuthLogService extends IService { + + /** + * 分页查询系统授权日志表 + * + * @return 系统授权日志表分页结果 {@link AuthLogVo} + */ + PageResult getAuthLogPage(Page pageParams, AuthLogDto dto); + + /** + * 根据id查询系统授权日志表详情 + * + * @param id 主键 + * @return 系统授权日志表详情 AuthLogVo} + */ + AuthLogVo getAuthLogById(Long id); + + /** + * 添加系统授权日志表 + * + * @param dto {@link AuthLogDto} 添加表单 + */ + void addAuthLog(AuthLogDto dto); + + /** + * 更新系统授权日志表 + * + * @param dto {@link AuthLogDto} 更新表单 + */ + void updateAuthLog(AuthLogDto dto); + + /** + * 删除|批量删除系统授权日志表类型 + * + * @param ids 删除id列表 + */ + void deleteAuthLog(List ids); +} diff --git a/spring-security/step-3/src/main/java/com/spring/step3/service/log/impl/AuthLogServiceImpl.java b/spring-security/step-3/src/main/java/com/spring/step3/service/log/impl/AuthLogServiceImpl.java new file mode 100644 index 0000000..62c8054 --- /dev/null +++ b/spring-security/step-3/src/main/java/com/spring/step3/service/log/impl/AuthLogServiceImpl.java @@ -0,0 +1,101 @@ +package com.spring.step3.service.log.impl; + +import com.baomidou.dynamic.datasource.annotation.DS; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.spring.step3.domain.dto.AuthLogDto; +import com.spring.step3.domain.entity.AuthLogEntity; +import com.spring.step3.domain.vo.AuthLogVo; +import com.spring.step3.domain.vo.result.PageResult; +import com.spring.step3.mapper.AuthLogMapper; +import com.spring.step3.service.log.AuthLogService; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +/** + *

+ * 系统授权日志表 服务实现类 + *

+ * + * @author Bunny + * @since 2025-07-19 14:26:58 + */ +@DS("testJwt") +@Service +@Transactional +public class AuthLogServiceImpl extends ServiceImpl implements AuthLogService { + + /** + * 系统授权日志表 服务实现类 + * + * @param pageParams 系统授权日志表分页查询page对象 + * @param dto 系统授权日志表分页查询对象 + * @return 查询分页系统授权日志表返回对象 + */ + @Override + public PageResult getAuthLogPage(Page pageParams, AuthLogDto dto) { + IPage page = baseMapper.selectListByPage(pageParams, dto); + + return PageResult.builder() + .list(page.getRecords()) + .pageNo(page.getCurrent()) + .pageSize(page.getSize()) + .total(page.getTotal()) + .build(); + } + + /** + * 根据id查询系统授权日志表详情 + * + * @param id 主键 + * @return 系统授权日志表详情 AuthLogVo} + */ + public AuthLogVo getAuthLogById(Long id) { + AuthLogEntity authLogEntity = getById(id); + + AuthLogVo authLogVo = new AuthLogVo(); + BeanUtils.copyProperties(authLogEntity, authLogVo); + + return authLogVo; + } + + /** + * 添加系统授权日志表 + * + * @param dto 系统授权日志表添加 + */ + @Override + public void addAuthLog(AuthLogDto dto) { + AuthLogEntity authLog = new AuthLogEntity(); + BeanUtils.copyProperties(dto, authLog); + + save(authLog); + } + + /** + * 更新系统授权日志表 + * + * @param dto 系统授权日志表更新 + */ + @Override + public void updateAuthLog(AuthLogDto dto) { + AuthLogEntity authLog = new AuthLogEntity(); + BeanUtils.copyProperties(dto, authLog); + + updateById(authLog); + } + + /** + * 删除|批量删除系统授权日志表 + * + * @param ids 删除id列表 + */ + @Override + public void deleteAuthLog(List ids) { + removeByIds(ids); + } +} \ No newline at end of file diff --git a/spring-security/step-3/src/main/resources/application-security.yml b/spring-security/step-3/src/main/resources/application-security.yml new file mode 100644 index 0000000..9c87577 --- /dev/null +++ b/spring-security/step-3/src/main/resources/application-security.yml @@ -0,0 +1,13 @@ +security-path: + secured-paths: + - "/api/**" + no-auth-paths: + - "/*/login" + # - "/api/security/**" + - "/api/permission/**" + - "/api/role/**" + - "/api/role-permission/**" + - "/api/user/**" + - "/api/user-role/**" + admin-authorities: + - "ADMIN" diff --git a/spring-security/step-3/src/main/resources/application.yml b/spring-security/step-3/src/main/resources/application.yml index 2b2e943..107e28d 100644 --- a/spring-security/step-3/src/main/resources/application.yml +++ b/spring-security/step-3/src/main/resources/application.yml @@ -6,6 +6,8 @@ spring: name: spring-security profiles: active: dev + include: + - security devtools: livereload: port: 0 @@ -54,4 +56,4 @@ jwtToken: # 主题 subject: SecurityBunny # 过期事件 7天 - expired: 604800 \ No newline at end of file + expired: 604800 diff --git a/spring-security/step-3/src/main/resources/mapper/AuthLogMapper.xml b/spring-security/step-3/src/main/resources/mapper/AuthLogMapper.xml new file mode 100644 index 0000000..9316d07 --- /dev/null +++ b/spring-security/step-3/src/main/resources/mapper/AuthLogMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + id, event_type,username,user_id,request_ip,request_method,request_uri,class_name,method_name,method_params,required_authority,user_authorities,decision_reason,exception_message,is_deleted, create_time, update_time, create_user, update_user + + + + + +