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 releaseOPTIONS
and白名单
request, it will callsuper.beforeInvocation(fi)
method, this method will call theAccessDecisionManager
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 thedecide
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 abovedecide
method is obtained from theFilterInvocationSecurityMetadataSource
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();
}
}
- 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
, Much simpler!
/**
* 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.
- mall integrates SpringSecurity and JWT to realize authentication and authorization (1)
- mall integrates SpringSecurity and JWT to realize authentication and authorization (2)
- It only takes four steps to integrate SpringSecurity+JWT to achieve login authentication!
- Teach you how to manage permissions and implement dynamic permission control of interfaces with Spring Security!
Project source code address
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-security
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。