The new version of Spring Security is elegant enough to use!

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 combat e-commerce project mall (50k+star) address: github.com/macrozheng/…

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 version 2.7.0.

org.springframework.boot
spring-boot-starter-parent
2.7.0



old usage


In versions prior to Spring Boot 2.7.0, we needed to write a configuration class to inherit WebSecurityConfigurerAdapter, and then rewrite the three methods in the Adapter for configuration;
/**
* SpringSecurity configuration
* 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 {
//Omit the configuration of 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 SecurityFilterChainBean, and move the original HttpSecurity configuration to this method.
/**
* New usage configuration for SpringSecurity 5.4.x and above
* To avoid circular dependencies, only used to configure HttpSecurity
* Created by macro on 2022/5/19.
*/
@Configuration
public class SecurityConfig {

@Bean
SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//Omit the configuration of HttpSecurity
return httpSecurity.build();
}

}

The new usage feels very simple and straightforward, avoiding the operation of inheriting WebSecurityConfigurerAdapter and rewriting 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 configuration
* 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;

/**
* Commodity Management Controller
* Created by macro on 2018/4/26.
*/
@Controller
@Api(tags = "PmsProductController", description = "Product Management")
@RequestMapping("/product")
public class PmsProductController {
@Autowired
private PmsProductService productService;

@ApiOperation("Create an item")
@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 method is convenient to implement, it is not an elegant method to write the permission value on the method.

/**
* UmsAdminService implementation class
* Created by macro on 2018/4/26.
*/
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
@Override
public UserDetails loadUserByUsername(String username){
//get user information
UmsAdmin admin = getAdminByUsername(username);
if (admin != null) {
List permissionList = getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("Incorrect username or password");
}
}

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. Pay attention to the doFilter method here, which is used to configure and release OPTIONS and whitelist requests. It will call the super.beforeInvocation(fi) method, which will call the decide method in AccessDecisionManager for authentication. right to operate;

/**
* Dynamic permission filter for path-based dynamic permission filtering
* 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 request is released directly
if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
//The whitelist request is released directly
PathMatcher pathMatcher = new AntPathMatcher();
for (String path : ignoreUrlsConfig.getUrls()) {
if(pathMatcher.match(path,request.getRequestURI())){
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
return;
}
}
//Here will call the decide method in AccessDecisionManager for authentication operation
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 the AccessDecisionManager, and use the decide method to match the permissions required by the access interface with the permissions the user has, and the match will be released;

/**
* Dynamic permission decision manager, used to determine whether the user has access rights
* Created by macro on 2020/2/7.
*/
public class DynamicAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication authentication, Object object,
Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
// Release directly when the interface is not configured with resources
if (CollUtil.isEmpty(configAttributes)) {
return;
}
Iterator iterator = configAttributes.iterator();
while (iterator.hasNext()) {
ConfigAttribute configAttribute = iterator.next();
// Compare access to required resources or user-owned resources
String needAuthority = configAttribute.getAttribute();
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("Sorry, you don't have access rights");
}

@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 getAttributes method of FilterInvocationSecurityMetadataSource, we also need to create a class to inherit it, and the getAttributes method can be used to obtain the permission value required to access the current path;

/**
* Dynamic permission data source for obtaining dynamic permission rules
* Created by macro on 2020/2/7.
*/
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

private static Map configAttributeMap = null;
@Autowired
private DynamicSecurityService dynamicSecurityService;

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

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

@Override
public Collection getAttributes(Object o) throws IllegalArgumentException {
if (configAttributeMap == null) this.loadDataSource();
List configAttributes = new ArrayList<>();
//Get the currently accessed path
String url = ((FilterInvocation) o).getRequestUrl();
String path = URLUtil.getPath(url);
PathMatcher pathMatcher = new AntPathMatcher();
Iterator iterator = configAttributeMap.keySet().iterator();
//Get the resources needed to access the path
while (iterator.hasNext()) {
String pattern = iterator.next();
if (pathMatcher.match(pattern, path)) {
configAttributes.add(configAttributeMap.get(pattern));
}
}
// The operation request permission is not set, return an empty collection
return configAttributes;
}

@Override
public Collection 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;

/**
* Dynamic permission related business class
* Created by macro on 2020/2/7.
*/
public interface DynamicSecurityService {
/**
* Load resource ANT wildcard and resource corresponding MAP
*/
Map loadDataSource();
}


Everything is ready, add the dynamic permission filter before FilterSecurityInterceptor;

/**
* New usage configuration for SpringSecurity 5.4.x and above
* To avoid circular dependencies, only used to configure 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 {
//Omit some configuration...
//Add dynamic permission verification filter when there is dynamic permission configuration
if(dynamicSecurityService!=null){
registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
}
return httpSecurity.build();
}

}


If you have read this article, it only takes four steps to integrate SpringSecurity+JWT to achieve login authentication! If so, you know that you should configure these two beans, one is responsible for obtaining the login user information, and the other is responsible for obtaining the stored dynamic permission rules. In order to adapt to the new usage of Spring Security, we no longer inherit SecurityConfig, which is much simpler!

/**
* mall-security module related configuration
* Custom configuration to configure how to obtain user information and dynamic permissions
* Created by macro on 2022/5/20.
*/
@Configuration
public class MallSecurityConfig {

@Autowired
private UmsAdminService adminService;

@Bean
public UserDetailsService userDetailsService() {
//Get the login user information
return username -> {
AdminUserDetails admin = adminService.getAdminByUsername(username);
if (admin != null) {
return admin;
}
throw new UsernameNotFoundException("Incorrect username or password");
};
}

@Bean
public DynamicSecurityService dynamicSecurityService() {
return new DynamicSecurityService() {
@Override
public Map loadDataSource() {
Map map = new ConcurrentHashMap<>();
List 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 sample project mall-tiny-security, log in with the following account and password, this account is only configured to access /brand/listAll, and the access address is: 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!

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00