0


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

版本以后,新建项目,导入项目需要的相关依赖:

<projectxmlns="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. 创建两个测试接口

  • 创建两个接口用于测试,源码参考如下importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassSecurityController{@GetMapping("/hello")publicStringhello(){return"hello, spring security.";}@PostMapping("/work")publicStringwork(){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

方法中,进行前端输入的用户信息和加载的用户信息进行信息对比。

importlombok.extern.java.Log;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UsernameNotFoundException;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.provisioning.UserDetailsManager;importorg.springframework.stereotype.Component;importjava.util.ArrayList;importjava.util.List;@Component@LogpublicclassMyUserDetailsManagerimplementsUserDetailsManager{@OverridepublicvoidcreateUser(UserDetails user){}@OverridepublicvoidupdateUser(UserDetails user){}@OverridepublicvoiddeleteUser(String username){}@OverridepublicvoidchangePassword(String oldPassword,String newPassword){}@OverridepublicbooleanuserExists(String username){returnfalse;}@OverridepublicUserDetailsloadUserByUsername(String username)throwsUsernameNotFoundException{/**
         * 这里为了演示方便,模拟从数据库查询,直接设置一下权限
         */
        log.info("query user from db!");returnqueryFromDB(username);}privatestaticUserDetailsqueryFromDB(String username){GrantedAuthority authority =newSimpleGrantedAuthority("testRole");List<GrantedAuthority> list =newArrayList<>();
        list.add(authority);returnnewUser("jack",// 用户名称newBCryptPasswordEncoder().encode("123456"),//密码
                list      //权限列表);}}

5. 定义权限不足处理逻辑

用户在访问没有权限的接口时,会抛出异常,

spring security

允许我们自己这里这种异常,我这里就是模拟一下权限不足的提示信息,不做过多处理。

importjakarta.servlet.ServletException;importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importorg.springframework.security.access.AccessDeniedException;importorg.springframework.security.web.access.AccessDeniedHandler;importorg.springframework.stereotype.Component;importjava.io.IOException;importjava.io.PrintWriter;@ComponentpublicclassMyAccessDeniedHandlerimplementsAccessDeniedHandler{@Overridepublicvoidhandle(HttpServletRequest request,HttpServletResponse response,AccessDeniedException accessDeniedException)throwsIOException,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

,提示用户登录。

importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.AuthenticationEntryPoint;importorg.springframework.stereotype.Component;importjava.io.IOException;importjava.io.PrintWriter;importjava.io.Serializable;@ComponentpublicclassMyAuthenticationEntryPointimplementsAuthenticationEntryPoint,Serializable{@Overridepublicvoidcommence(HttpServletRequest request,HttpServletResponse response,AuthenticationException authException)throwsIOException{//验证为未登陆状态会进入此方法,认证错误
        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. 定义自定义动态权限检验处理逻辑

在请求接口进行安全访问的时候,我们可以指定访问接口需要的角色,但是实际应用中,为了满足系统的灵活性,我们往往需要自定义动态权限的校验逻辑。

importorg.springframework.security.authorization.AuthorizationDecision;importorg.springframework.security.authorization.AuthorizationManager;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.web.access.intercept.RequestAuthorizationContext;importorg.springframework.stereotype.Component;importjava.util.Collection;importjava.util.function.Supplier;@ComponentpublicclassMyAuthorizationManagerimplementsAuthorizationManager<RequestAuthorizationContext>{/**
     * @param authentication the {@link Supplier} of the {@link Authentication} to check
     * @param object         the {@link T} object to check
     * @return
     */@OverridepublicAuthorizationDecisioncheck(Supplier<Authentication> authentication,RequestAuthorizationContext object){// 获取访问urlString requestURI = object.getRequest().getRequestURI();// 模拟从数据库或者缓存里面查询拥有当前URI的权限的角色String[] allRole =query(requestURI);// 获取当前用户权限Collection<?extendsGrantedAuthority> authorities = authentication.get().getAuthorities();// 判断是否拥有权限for(String role : allRole){for(GrantedAuthority r : authorities){if(role.equals(r.getAuthority())){returnnewAuthorizationDecision(true);// 返回有权限}}}returnnewAuthorizationDecision(false);//返回没有权限}/**
     * 查询当前拥有对应url的权限的角色
     *
     * @param requestURI
     * @return
     */privateString[]query(String requestURI){returnnewString[]{"testRole"};}}

8. 定义安全访问统一入口

在统一入口,我们可以做一些统一的逻辑,比如前后端分离的情况下,进行

token

内容的解析,这里我只是用代码模拟演示一下,方便大家理解。

importjakarta.servlet.FilterChain;importjakarta.servlet.ServletException;importjakarta.servlet.http.HttpServletRequest;importjakarta.servlet.http.HttpServletResponse;importlombok.extern.java.Log;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.stereotype.Component;importorg.springframework.util.StringUtils;importorg.springframework.web.filter.OncePerRequestFilter;importjava.io.IOException;importjava.util.ArrayList;importjava.util.List;importjava.util.logging.Level;@Component@LogpublicclassMyAuthenticationFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain)throwsServletException,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);}privateUsernamePasswordAuthenticationTokengetUserFromToken(String token){GrantedAuthority authority =newSimpleGrantedAuthority(token);List<GrantedAuthority> list =newArrayList<>();
        list.add(authority);User user =newUser("jack",// 用户名称newBCryptPasswordEncoder().encode("123456"),//密码
                list      //权限列表);UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =newUsernamePasswordAuthenticationToken(user,null, user.getAuthorities());
        usernamePasswordAuthenticationToken.setDetails(user);return usernamePasswordAuthenticationToken;}}

9. 编写spring security配置类

当所有准备工作,做好以后,下面就是编写

spring security

的配置类了,使我们的相关配置生效。

importcom.j.ss.MyAccessDeniedHandler;importcom.j.ss.MyAuthenticationEntryPoint;importcom.j.ss.MyAuthenticationFilter;importcom.j.ss.MyAuthorizationManager;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;importorg.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;importorg.springframework.security.config.annotation.web.configurers.HeadersConfigurer;importorg.springframework.security.config.http.SessionCreationPolicy;importorg.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;/**
 * @Configuration 注解表示将该类以配置类的方式注册到spring容器中
 */@Configuration/**
 * @EnableWebSecurity 注解表示启动spring security
 */@EnableWebSecurity/**
 * @EnableMethodSecurity 注解表示启动全局函数权限
 */@EnableMethodSecuritypublicclassWebSecurityConfig{/**
     * 权限不足处理逻辑
     */@AutowiredprivateMyAccessDeniedHandler accessDeniedHandler;/**
     * 未授权处理逻辑
     */@AutowiredprivateMyAuthenticationEntryPoint authenticationEntryPoint;/**
     * 访问统一处理器
     */@AutowiredprivateMyAuthenticationFilter authenticationTokenFilter;/**
     * 自定义权限校验逻辑
     */@AutowiredprivateMyAuthorizationManager myAuthorizationManager;/**
     * spring security的核心过滤器链
     *
     * @param httpSecurity
     * @return
     */@BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurity httpSecurity)throwsException{// 定义安全请求拦截规则
        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
     */@BeanpublicPasswordEncoderpasswordEncoder(){returnnewBCryptPasswordEncoder();}/**
     * 忽略权限校验
     *
     * @return
     */@BeanpublicWebSecurityCustomizerwebSecurityCustomizer(){return(web -> web.ignoring().requestMatchers("/hello"));}}

三、 功能测试

上述代码编写完成以后,启动项目,下面进行功能测试。

1. 忽略权限校验测试

访问

/hello

接口

在这里插入图片描述

可以看到,此时接口在无登录信息的情况下,也可以正常访问的。

2. 无权限测试

同样的,我们直接访问

/work

接口

在这里插入图片描述

可以看到,此时提醒我们需要登录了。

3. 有权限测试

再次访问

/work

接口,模拟已经登录,并拥有对应的权限。

在这里插入图片描述

可以看到,我们模拟有

testRole

权限,此时访问是正常的。

4. 权限不足测试

再次访问

/work

接口,模拟已经登录,但拥有错误的权限。

在这里插入图片描述

可以看到,此时报出了权限不足的异常。

四、写在最后

上面的案例只是演示,

spring security

的实际应用,应该根据具体项目权限要求来进行合理实现。


本文转载自: https://blog.csdn.net/C_AJing/article/details/140276668
版权归原作者 代码探险家_cool 所有, 如有侵权,请联系我们删除。

“spring boot 3.x版本中集成spring security 6.x版本进行实现动态权限控制解决方案”的评论:

还没有评论