4
头图
Not long ago, Spring Boot 2.7.0 was released, and Spring Security was also upgraded to 5.7.1. After the upgrade, I found that the Spring Security configuration method that I had been using had been deprecated. I can't help feeling that the technology update is so fast, and it is abandoned when it is used! Today, I will show you the latest usage of Spring Security to see if it is elegant enough!

SpringBoot actual e-commerce project mall (50k+star) address: https://github.com/macrozheng/mall

basic use

Let's first compare the basic function login authentication provided by Spring Security to see if the new version is better.

upgraded version

First, modify the pom.xml file of the project and upgrade the Spring Boot version to the 2.7.0 version.

 <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

old usage

In versions before Spring Boot 2.7.0, we need to write a configuration class to inherit WebSecurityConfigurerAdapter , and then rewrite the three methods in the Adapter for configuration;

 /**
 * SpringSecurity的配置
 * Created by macro on 2018/4/26.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UmsAdminService adminService;

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //省略HttpSecurity的配置
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());
    }
    
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

If you use it in SpringBoot version 2.7.0, you will find that WebSecurityConfigurerAdapter has been deprecated, and it seems that Spring Security will resolutely abandon this usage!

new usage

The new usage is very simple, no need to inherit WebSecurityConfigurerAdapter , just declare the configuration class directly, configure a method to generate SecurityFilterChain Bean, and move the original HttpSecurity configuration to this method .

 /**
 * SpringSecurity 5.4.x以上新用法配置
 * 为避免循环依赖,仅用于配置HttpSecurity
 * Created by macro on 2022/5/19.
 */
@Configuration
public class SecurityConfig {

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        //省略HttpSecurity的配置
        return httpSecurity.build();
    }

}

The new usage feels very concise and straightforward, avoiding inheritance WebSecurityConfigurerAdapter and rewriting the operation of the method. It is strongly recommended that you update it!

Advanced use

After upgrading the Spring Boot 2.7.0 version, Spring Security has made major changes to the configuration method, so is there any impact on other uses? In fact, it has no effect. Let's talk about how to use Spring Security to implement dynamic permission control!

Method-based dynamic permissions

First, let's talk about method-based dynamic permission control. Although this method is simple to implement, it has certain drawbacks.
  • Use @EnableGlobalMethodSecurity on the configuration class to enable it;
 /**
 * SpringSecurity的配置
 * Created by macro on 2018/4/26.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
    
}
  • Then use @PreAuthorize in the method to configure the permissions required to access the interface;
 /**
 * 商品管理Controller
 * Created by macro on 2018/4/26.
 */
@Controller
@Api(tags = "PmsProductController", description = "商品管理")
@RequestMapping("/product")
public class PmsProductController {
    @Autowired
    private PmsProductService productService;

    @ApiOperation("创建商品")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:product:create')")
    public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) {
        int count = productService.create(productParam);
        if (count > 0) {
            return CommonResult.success(count);
        } else {
            return CommonResult.failed();
        }
    }
}
  • Then query the user's permission value from the database and set it to the UserDetails object. Although this approach is easy to implement, it is not an elegant approach to write the permission value on the method. .
 /**
 * UmsAdminService实现类
 * Created by macro on 2018/4/26.
 */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    @Override
    public UserDetails loadUserByUsername(String username){
        //获取用户信息
        UmsAdmin admin = getAdminByUsername(username);
        if (admin != null) {
            List<UmsPermission> permissionList = getPermissionList(admin.getId());
            return new AdminUserDetails(admin,permissionList);
        }
        throw new UsernameNotFoundException("用户名或密码错误");
    }
}

Path-based dynamic permissions

In fact, the path corresponding to each interface is unique, and it is more elegant to control the permissions of the interface through the path.
  • First we need to create a filter for dynamic permissions, here pay attention to the doFilter method, which is used to configure the release OPTIONS and 白名单 request, it will call super.beforeInvocation(fi) method, this method will call the AccessDecisionManager decide for authentication operation;
 /**
 * 动态权限过滤器,用于实现基于路径的动态权限过滤
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {

    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;

    @Autowired
    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //OPTIONS请求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        //白名单请求直接放行
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : ignoreUrlsConfig.getUrls()) {
            if(pathMatcher.match(path,request.getRequestURI())){
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return;
            }
        }
        //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }

    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return dynamicSecurityMetadataSource;
    }

}
  • Next, we need to create a class to inherit AccessDecisionManager , and use the decide method to match the permissions required to access the interface and the permissions the user has, and the match will be released;
 /**
 * 动态权限决策管理器,用于判断用户是否有访问权限
 * Created by macro on 2020/2/7.
 */
public class DynamicAccessDecisionManager implements AccessDecisionManager {

    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 当接口未被配置资源时直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //将访问所需资源或用户拥有资源进行比对
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您没有访问权限");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}
  • Since the configAttributes attribute in the above decide method is obtained from the FilterInvocationSecurityMetadataSource getAttributes method, we need to create a class inherited from the ---b9c147320ec9f239a6966a129--- method. It, getAttributes method can be used to obtain the permission value required to access the current path;
 /**
 * 动态权限数据源,用于获取动态权限规则
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static Map<String, ConfigAttribute> configAttributeMap = null;
    @Autowired
    private DynamicSecurityService dynamicSecurityService;

    @PostConstruct
    public void loadDataSource() {
        configAttributeMap = dynamicSecurityService.loadDataSource();
    }

    public void clearDataSource() {
        configAttributeMap.clear();
        configAttributeMap = null;
    }

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (configAttributeMap == null) this.loadDataSource();
        List<ConfigAttribute>  configAttributes = new ArrayList<>();
        //获取当前访问的路径
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = URLUtil.getPath(url);
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = configAttributeMap.keySet().iterator();
        //获取访问该路径所需资源
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if (pathMatcher.match(pattern, path)) {
                configAttributes.add(configAttributeMap.get(pattern));
            }
        }
        // 未设置操作请求权限,返回空集合
        return configAttributes;
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}
  • It should be noted here that the permission value data corresponding to all paths comes from the custom DynamicSecurityService ;
 /**
 * 动态权限相关业务类
 * Created by macro on 2020/2/7.
 */
public interface DynamicSecurityService {
    /**
     * 加载资源ANT通配符和资源对应MAP
     */
    Map<String, ConfigAttribute> loadDataSource();
}
  • Everything is ready, add the dynamic permission filter before FilterSecurityInterceptor ;
 /**
 * SpringSecurity 5.4.x以上新用法配置
 * 为避免循环依赖,仅用于配置HttpSecurity
 * Created by macro on 2022/5/19.
 */
@Configuration
public class SecurityConfig {
    
    @Autowired
    private DynamicSecurityService dynamicSecurityService;
    @Autowired
    private DynamicSecurityFilter dynamicSecurityFilter;

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        //省略若干配置...
        //有动态权限配置时添加动态权限校验过滤器
        if(dynamicSecurityService!=null){
            registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
        }
        return httpSecurity.build();
    }

}
 /**
 * mall-security模块相关配置
 * 自定义配置,用于配置如何获取用户信息及动态权限
 * Created by macro on 2022/5/20.
 */
@Configuration
public class MallSecurityConfig {

    @Autowired
    private UmsAdminService adminService;

    @Bean
    public UserDetailsService userDetailsService() {
        //获取登录用户信息
        return username -> {
            AdminUserDetails admin = adminService.getAdminByUsername(username);
            if (admin != null) {
                return admin;
            }
            throw new UsernameNotFoundException("用户名或密码错误");
        };
    }

    @Bean
    public DynamicSecurityService dynamicSecurityService() {
        return new DynamicSecurityService() {
            @Override
            public Map<String, ConfigAttribute> loadDataSource() {
                Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
                List<UmsResource> resourceList = adminService.getResourceList();
                for (UmsResource resource : resourceList) {
                    map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
                }
                return map;
            }
        };
    }

}

effect test

  • Next, start our example project mall-tiny-security , log in with the following account and password, this account is only configured with the permission to access /brand/listAll , the access address: http://localhost:8088/swagger- ui/

  • Then put the returned token into the authentication header of Swagger;

  • When we access the authorized interface, the data can be obtained normally;

  • When we access the interface without permission, return the interface prompt without access permission.

Summarize

The upgrade usage of Spring Security is indeed elegant enough, simple enough, and more compatible with previous usage! Personally, I feel that a mature framework will not change its usage greatly during the upgrade process. Even if it is changed, it will be compatible with the previous usage, so for most frameworks, the old version will be used, and the new version will still be used!

References

This article is only a summary of the new usage of Spring Security. If you want to know more about the usage of Spring Security, you can refer to the previous articles.

Project source code address

https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-security


macrozheng
1.1k 声望1.3k 粉丝