一、背景
最近在进行项目从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 {@Overridepublic void createUser(UserDetails user) {}@Overridepublic void updateUser(UserDetails user) {}@Overridepublic void deleteUser(String username) {}@Overridepublic void changePassword(String oldPassword, String newPassword) {}@Overridepublic boolean userExists(String username) {return false;}@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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*/@Overridepublic AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {// 获取访问urlString 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 {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token"); // 前后端分离的时候获取tokenif (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 {/*** 权限不足处理逻辑*/@Autowiredprivate MyAccessDeniedHandler accessDeniedHandler;/*** 未授权处理逻辑*/@Autowiredprivate MyAuthenticationEntryPoint authenticationEntryPoint;/*** 访问统一处理器*/@Autowiredprivate MyAuthenticationFilter authenticationTokenFilter;/*** 自定义权限校验逻辑*/@Autowiredprivate MyAuthorizationManager myAuthorizationManager;/*** spring security的核心过滤器链** @param httpSecurity* @return*/@Beanpublic 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*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 忽略权限校验** @return*/@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return (web -> web.ignoring().requestMatchers("/hello"));}}
三、 功能测试
上述代码编写完成以后,启动项目,下面进行功能测试。
1. 忽略权限校验测试
访问/hello接口

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

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

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

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