spring boot 3.x版本下使用spring security 6.x版本实现动态权限控制
一、背景
最近在进行项目从jdk8和spring boot 2.7.x版本技术架构向jdk17和spring boot 3.3.x版本的代码迁移,在迁移过程中,发现spring boot 3.3.x版本依赖的spring security版本已经升级6.x版本了,语法上和spring security 5.x版本有很多地方不兼容,因此记录试一下spring boot 3.3.x版本下,spring security 6.x的集成方案。
二、技术实现
1. 创建spring boot 3.3.x版本项目
spring boot 3.3.x版本对jdk版本要求较高,我这里使用的是jdk17,不久前,jdk21也已经发布了,可以支持虚拟线程,大家也可以使用jdk21。
设置好jdk版本以后,新建项目,导入项目需要的相关依赖:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.1</version>
</parent>
<groupId>com.j.ss</groupId>
<artifactId>spring-secrity6-spring-boot3-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-secrity6-spring-boot3-demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
2. 创建两个测试接口
-
创建两个接口用于测试,源码参考如下
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class SecurityController { @GetMapping("/hello") public String hello() { return "hello, spring security."; } @PostMapping("/work") public String work() { return "I am working."; } } -
启动项目,测试一下接口是否正常
- hello接口

- work接口

3. 引入spring-boot-starter-security依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入spring-boot-starter-security依赖以后,此时访问接口,会有未授权问题。

4. 定义UserDetailsManager实现类
spring security框架会自动使用UserDetailsManager的loadUserByUsername方法进行用户加载,在加载用户以后,会在UsernamePasswordAuthenticationFilter过滤器中的attemptAuthentication方法中,进行前端输入的用户信息和加载的用户信息进行信息对比。
import lombok.extern.java.Log;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
@Log
public class MyUserDetailsManager implements UserDetailsManager {
@Override
public void createUser(UserDetails user) {
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 这里为了演示方便,模拟从数据库查询,直接设置一下权限
*/
log.info("query user from db!");
return queryFromDB(username);
}
private static UserDetails queryFromDB(String username) {
GrantedAuthority authority = new SimpleGrantedAuthority("testRole");
List<GrantedAuthority> list = new ArrayList<>();
list.add(authority);
return new User("jack", // 用户名称
new BCryptPasswordEncoder().encode("123456"), //密码
list //权限列表
);
}
}
5. 定义权限不足处理逻辑
用户在访问没有权限的接口时,会抛出异常,spring security允许我们自己这里这种异常,我这里就是模拟一下权限不足的提示信息,不做过多处理。
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 org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//登陆状态下,权限不足执行该方法
response.setStatus(200);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
String body = "403,权限不足!";
printWriter.write(body);
printWriter.flush();
}
}
6. 定义未登录情况处理逻辑
当用户没有登录情况下,访问需要权限的接口时,会抛出异常,spring security允许我们自定义处理逻辑,这里未登录就直接抛出401,提示用户登录。
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
//验证为未登陆状态会进入此方法,认证错误
response.setStatus(401);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = response.getWriter();
String body = "401, 请先进行登录!";
printWriter.write(body);
printWriter.flush();
}
}
7. 定义自定义动态权限检验处理逻辑
在请求接口进行安全访问的时候,我们可以指定访问接口需要的角色,但是实际应用中,为了满足系统的灵活性,我们往往需要自定义动态权限的校验逻辑。
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.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.function.Supplier;
@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
/**
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param object the {@link T} object to check
* @return
*/
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
// 获取访问url
String requestURI = object.getRequest().getRequestURI();
// 模拟从数据库或者缓存里面查询拥有当前URI的权限的角色
String[] allRole = query(requestURI);
// 获取当前用户权限
Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
// 判断是否拥有权限
for (String role : allRole) {
for (GrantedAuthority r : authorities) {
if (role.equals(r.getAuthority())) {
return new AuthorizationDecision(true); // 返回有权限
}
}
}
return new AuthorizationDecision(false); //返回没有权限
}
/**
* 查询当前拥有对应url的权限的角色
*
* @param requestURI
* @return
*/
private String[] query(String requestURI) {
return new String[]{"testRole"};
}
}
8. 定义安全访问统一入口
在统一入口,我们可以做一些统一的逻辑,比如前后端分离的情况下,进行token内容的解析,这里我只是用代码模拟演示一下,方便大家理解。
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.java.Log;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
@Component
@Log
public class MyAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token"); // 前后端分离的时候获取token
if (StringUtils.hasText(token)) { // 如果token不为空,则需要解析出用户信息,填充到当前上下文中
UsernamePasswordAuthenticationToken authentication = getUserFromToken(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
if (log.isLoggable(Level.INFO)) {
log.info("set authentication");
}
} else {
if (log.isLoggable(Level.INFO)) {
log.info("user info is null.");
}
}
filterChain.doFilter(request, response);
}
private UsernamePasswordAuthenticationToken getUserFromToken(String token) {
GrantedAuthority authority = new SimpleGrantedAuthority(token);
List<GrantedAuthority> list = new ArrayList<>();
list.add(authority);
User user = new User("jack", // 用户名称
new BCryptPasswordEncoder().encode("123456"), //密码
list //权限列表
);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(user);
return usernamePasswordAuthenticationToken;
}
}
9. 编写spring security配置类
当所有准备工作,做好以后,下面就是编写spring security的配置类了,使我们的相关配置生效。
import com.j.ss.MyAccessDeniedHandler;
import com.j.ss.MyAuthenticationEntryPoint;
import com.j.ss.MyAuthenticationFilter;
import com.j.ss.MyAuthorizationManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
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.authentication.UsernamePasswordAuthenticationFilter;
/**
* @Configuration 注解表示将该类以配置类的方式注册到spring容器中
*/
@Configuration
/**
* @EnableWebSecurity 注解表示启动spring security
*/
@EnableWebSecurity
/**
* @EnableMethodSecurity 注解表示启动全局函数权限
*/
@EnableMethodSecurity
public class WebSecurityConfig {
/**
* 权限不足处理逻辑
*/
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
/**
* 未授权处理逻辑
*/
@Autowired
private MyAuthenticationEntryPoint authenticationEntryPoint;
/**
* 访问统一处理器
*/
@Autowired
private MyAuthenticationFilter authenticationTokenFilter;
/**
* 自定义权限校验逻辑
*/
@Autowired
private MyAuthorizationManager myAuthorizationManager;
/**
* spring security的核心过滤器链
*
* @param httpSecurity
* @return
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 定义安全请求拦截规则
httpSecurity.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
authorizationManagerRequestMatcherRegistry
.requestMatchers("/hello")
.permitAll() // hello 接口放行,不进行权限校验
.anyRequest()
// .hasRole() 其他接口不进行role具体校验,进行动态权限校验
.access(myAuthorizationManager); // 动态权限校验逻辑
})
// 前后端分离,关闭csrf
.csrf(AbstractHttpConfigurer::disable)
// 前后端分离架构禁用session
.sessionManagement(httpSecuritySessionManagementConfigurer -> {
httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
})
// 访问异常处理
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
httpSecurityExceptionHandlingConfigurer.accessDeniedHandler(accessDeniedHandler);
})
// 未授权异常处理
.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(authenticationEntryPoint);
})
.headers(httpSecurityHeadersConfigurer -> {
// 禁用缓存
httpSecurityHeadersConfigurer.cacheControl(HeadersConfigurer.CacheControlConfig::disable);
httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
});
// 添加入口filter, 前后端分离的时候,可以进行token解析操作
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
/**
* 明文密码加密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 忽略权限校验
*
* @return
*/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/hello"));
}
}
三、 功能测试
上述代码编写完成以后,启动项目,下面进行功能测试。
1. 忽略权限校验测试
访问/hello接口

可以看到,此时接口在无登录信息的情况下,也可以正常访问的。
2. 无权限测试
同样的,我们直接访问/work接口

可以看到,此时提醒我们需要登录了。
3. 有权限测试
再次访问/work接口,模拟已经登录,并拥有对应的权限。

可以看到,我们模拟有testRole权限,此时访问是正常的。
4. 权限不足测试
再次访问/work接口,模拟已经登录,但拥有错误的权限。

可以看到,此时报出了权限不足的异常。
四、写在最后
上面的案例只是演示,spring security的实际应用,应该根据具体项目权限要求来进行合理实现。
转载自:https://juejin.cn/post/7391122567279394828