• 别再用过时的方式了!全新版本Spring Security,这样用才够优雅!


    基本使用

    我们先对比下Spring Security提供的基本功能登录认证,来看看新版用法是不是更好。

    升级版本

    首先修改项目的pom.xml文件,把Spring Boot版本升级至2.7.0版本。

    1. <parent>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-parent</artifactId>
    4. <version>2.7.0</version>
    5. <relativePath/> <!-- lookup parent from repository -->
    6. </parent>
    7. 复制代码

    旧用法

    在Spring Boot 2.7.0 之前的版本中,我们需要写个配置类继承WebSecurityConfigurerAdapter,然后重写Adapter中的三个方法进行配置;

    1. /**
    2. * SpringSecurity的配置
    3. * Created by macro on 2018/4/26.
    4. */
    5. @Configuration
    6. @EnableWebSecurity
    7. @EnableGlobalMethodSecurity(prePostEnabled = true)
    8. public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
    9. @Autowired
    10. private UmsAdminService adminService;
    11. @Override
    12. protected void configure(HttpSecurity httpSecurity) throws Exception {
    13. //省略HttpSecurity的配置
    14. }
    15. @Override
    16. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    17. auth.userDetailsService(userDetailsService())
    18. .passwordEncoder(passwordEncoder());
    19. }
    20. @Bean
    21. @Override
    22. public AuthenticationManager authenticationManagerBean() throws Exception {
    23. return super.authenticationManagerBean();
    24. }
    25. }
    26. 复制代码

    如果你在SpringBoot 2.7.0版本中进行使用的话,你就会发现WebSecurityConfigurerAdapter已经被弃用了,看样子Spring Security要坚决放弃这种用法了!

    新用法

    新用法非常简单,无需再继承WebSecurityConfigurerAdapter,只需直接声明配置类,再配置一个生成SecurityFilterChainBean的方法,把原来的HttpSecurity配置移动到该方法中即可。

    1. /**
    2. * SpringSecurity 5.4.x以上新用法配置
    3. * 为避免循环依赖,仅用于配置HttpSecurity
    4. * Created by macro on 2022/5/19.
    5. */
    6. @Configuration
    7. public class SecurityConfig {
    8. @Bean
    9. SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    10. //省略HttpSecurity的配置
    11. return httpSecurity.build();
    12. }
    13. }
    14. 复制代码

    新用法感觉非常简洁干脆,避免了继承WebSecurityConfigurerAdapter并重写方法的操作,强烈建议大家更新一波!

    高级使用

    升级 Spring Boot 2.7.0版本后,Spring Security对于配置方法有了大的更改,那么其他使用有没有影响呢?其实是没啥影响的,这里再聊聊如何使用Spring Security实现动态权限控制!

    基于方法的动态权限

    首先来聊聊基于方法的动态权限控制,这种方式虽然实现简单,但却有一定的弊端。

    • 在配置类上使用@EnableGlobalMethodSecurity来开启它;
    1. /**
    2. * SpringSecurity的配置
    3. * Created by macro on 2018/4/26.
    4. */
    5. @Configuration
    6. @EnableWebSecurity
    7. @EnableGlobalMethodSecurity(prePostEnabled = true)
    8. public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
    9. }
    10. 复制代码
    • 然后在方法中使用@PreAuthorize配置访问接口需要的权限;
    1. /**
    2. * 商品管理Controller
    3. * Created by macro on 2018/4/26.
    4. */
    5. @Controller
    6. @Api(tags = "PmsProductController", description = "商品管理")
    7. @RequestMapping("/product")
    8. public class PmsProductController {
    9. @Autowired
    10. private PmsProductService productService;
    11. @ApiOperation("创建商品")
    12. @RequestMapping(value = "/create", method = RequestMethod.POST)
    13. @ResponseBody
    14. @PreAuthorize("hasAuthority('pms:product:create')")
    15. public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) {
    16. int count = productService.create(productParam);
    17. if (count > 0) {
    18. return CommonResult.success(count);
    19. } else {
    20. return CommonResult.failed();
    21. }
    22. }
    23. }
    24. 复制代码
    • 再从数据库中查询出用户所拥有的权限值设置到UserDetails对象中去,这种做法虽然实现方便,但是把权限值写死在了方法上,并不是一种优雅的做法。
    1. /**
    2. * UmsAdminService实现类
    3. * Created by macro on 2018/4/26.
    4. */
    5. @Service
    6. public class UmsAdminServiceImpl implements UmsAdminService {
    7. @Override
    8. public UserDetails loadUserByUsername(String username){
    9. //获取用户信息
    10. UmsAdmin admin = getAdminByUsername(username);
    11. if (admin != null) {
    12. List<UmsPermission> permissionList = getPermissionList(admin.getId());
    13. return new AdminUserDetails(admin,permissionList);
    14. }
    15. throw new UsernameNotFoundException("用户名或密码错误");
    16. }
    17. }
    18. 复制代码

    基于路径的动态权限

    其实每个接口对应的路径都是唯一的,通过路径来进行接口的权限控制才是更优雅的方式。

    • 首先我们需要创建一个动态权限的过滤器,这里注意下doFilter方法,用于配置放行OPTIONS白名单请求,它会调用super.beforeInvocation(fi)方法,此方法将调用AccessDecisionManager中的decide方法来进行鉴权操作;
    1. /**
    2. * 动态权限过滤器,用于实现基于路径的动态权限过滤
    3. * Created by macro on 2020/2/7.
    4. */
    5. public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
    6. @Autowired
    7. private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    8. @Autowired
    9. private IgnoreUrlsConfig ignoreUrlsConfig;
    10. @Autowired
    11. public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
    12. super.setAccessDecisionManager(dynamicAccessDecisionManager);
    13. }
    14. @Override
    15. public void init(FilterConfig filterConfig) throws ServletException {
    16. }
    17. @Override
    18. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    19. HttpServletRequest request = (HttpServletRequest) servletRequest;
    20. FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
    21. //OPTIONS请求直接放行
    22. if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
    23. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    24. return;
    25. }
    26. //白名单请求直接放行
    27. PathMatcher pathMatcher = new AntPathMatcher();
    28. for (String path : ignoreUrlsConfig.getUrls()) {
    29. if(pathMatcher.match(path,request.getRequestURI())){
    30. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    31. return;
    32. }
    33. }
    34. //此处会调用AccessDecisionManager中的decide方法进行鉴权操作
    35. InterceptorStatusToken token = super.beforeInvocation(fi);
    36. try {
    37. fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
    38. } finally {
    39. super.afterInvocation(token, null);
    40. }
    41. }
    42. @Override
    43. public void destroy() {
    44. }
    45. @Override
    46. public Class<?> getSecureObjectClass() {
    47. return FilterInvocation.class;
    48. }
    49. @Override
    50. public SecurityMetadataSource obtainSecurityMetadataSource() {
    51. return dynamicSecurityMetadataSource;
    52. }
    53. }
    54. 复制代码
    • 接下来我们就需要创建一个类来继承AccessDecisionManager,通过decide方法对访问接口所需权限和用户拥有的权限进行匹配,匹配则放行;
    1. /**
    2. * 动态权限决策管理器,用于判断用户是否有访问权限
    3. * Created by macro on 2020/2/7.
    4. */
    5. public class DynamicAccessDecisionManager implements AccessDecisionManager {
    6. @Override
    7. public void decide(Authentication authentication, Object object,
    8. Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
    9. // 当接口未被配置资源时直接放行
    10. if (CollUtil.isEmpty(configAttributes)) {
    11. return;
    12. }
    13. Iterator<ConfigAttribute> iterator = configAttributes.iterator();
    14. while (iterator.hasNext()) {
    15. ConfigAttribute configAttribute = iterator.next();
    16. //将访问所需资源或用户拥有资源进行比对
    17. String needAuthority = configAttribute.getAttribute();
    18. for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
    19. if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
    20. return;
    21. }
    22. }
    23. }
    24. throw new AccessDeniedException("抱歉,您没有访问权限");
    25. }
    26. @Override
    27. public boolean supports(ConfigAttribute configAttribute) {
    28. return true;
    29. }
    30. @Override
    31. public boolean supports(Class<?> aClass) {
    32. return true;
    33. }
    34. }
    35. 复制代码
    • 由于上面的decide方法中的configAttributes属性是从FilterInvocationSecurityMetadataSourcegetAttributes方法中获取的,我们还需创建一个类继承它,getAttributes方法可用于获取访问当前路径所需权限值;
    1. /**
    2. * 动态权限数据源,用于获取动态权限规则
    3. * Created by macro on 2020/2/7.
    4. */
    5. public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    6. private static Map<String, ConfigAttribute> configAttributeMap = null;
    7. @Autowired
    8. private DynamicSecurityService dynamicSecurityService;
    9. @PostConstruct
    10. public void loadDataSource() {
    11. configAttributeMap = dynamicSecurityService.loadDataSource();
    12. }
    13. public void clearDataSource() {
    14. configAttributeMap.clear();
    15. configAttributeMap = null;
    16. }
    17. @Override
    18. public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
    19. if (configAttributeMap == null) this.loadDataSource();
    20. List<ConfigAttribute> configAttributes = new ArrayList<>();
    21. //获取当前访问的路径
    22. String url = ((FilterInvocation) o).getRequestUrl();
    23. String path = URLUtil.getPath(url);
    24. PathMatcher pathMatcher = new AntPathMatcher();
    25. Iterator<String> iterator = configAttributeMap.keySet().iterator();
    26. //获取访问该路径所需资源
    27. while (iterator.hasNext()) {
    28. String pattern = iterator.next();
    29. if (pathMatcher.match(pattern, path)) {
    30. configAttributes.add(configAttributeMap.get(pattern));
    31. }
    32. }
    33. // 未设置操作请求权限,返回空集合
    34. return configAttributes;
    35. }
    36. @Override
    37. public Collection<ConfigAttribute> getAllConfigAttributes() {
    38. return null;
    39. }
    40. @Override
    41. public boolean supports(Class<?> aClass) {
    42. return true;
    43. }
    44. }
    45. 复制代码
    • 这里需要注意的是,所有路径对应的权限值数据来自于自定义的DynamicSecurityService
    1. /**
    2. * 动态权限相关业务类
    3. * Created by macro on 2020/2/7.
    4. */
    5. public interface DynamicSecurityService {
    6. /**
    7. * 加载资源ANT通配符和资源对应MAP
    8. */
    9. Map<String, ConfigAttribute> loadDataSource();
    10. }
    11. 复制代码
    • 一切准备就绪,把动态权限过滤器添加到FilterSecurityInterceptor之前;
    1. /**
    2. * SpringSecurity 5.4.x以上新用法配置
    3. * 为避免循环依赖,仅用于配置HttpSecurity
    4. * Created by macro on 2022/5/19.
    5. */
    6. @Configuration
    7. public class SecurityConfig {
    8. @Autowired
    9. private DynamicSecurityService dynamicSecurityService;
    10. @Autowired
    11. private DynamicSecurityFilter dynamicSecurityFilter;
    12. @Bean
    13. SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    14. //省略若干配置...
    15. //有动态权限配置时添加动态权限校验过滤器
    16. if(dynamicSecurityService!=null){
    17. registry.and().addFilterBefore(dynamicSecurityFilter, FilterSecurityInterceptor.class);
    18. }
    19. return httpSecurity.build();
    20. }
    21. }

    如果你看过这篇仅需四步,整合SpringSecurity+JWT实现登录认证 ! 的话,就知道应该要配置这两个Bean了,一个负责获取登录用户信息,另一个负责获取存储的动态权限规则,为了适应Spring Security的新用法,我们不再继承SecurityConfig,简洁了不少!

    1. /**
    2. * mall-security模块相关配置
    3. * 自定义配置,用于配置如何获取用户信息及动态权限
    4. * Created by macro on 2022/5/20.
    5. */
    6. @Configuration
    7. public class MallSecurityConfig {
    8. @Autowired
    9. private UmsAdminService adminService;
    10. @Bean
    11. public UserDetailsService userDetailsService() {
    12. //获取登录用户信息
    13. return username -> {
    14. AdminUserDetails admin = adminService.getAdminByUsername(username);
    15. if (admin != null) {
    16. return admin;
    17. }
    18. throw new UsernameNotFoundException("用户名或密码错误");
    19. };
    20. }
    21. @Bean
    22. public DynamicSecurityService dynamicSecurityService() {
    23. return new DynamicSecurityService() {
    24. @Override
    25. public Map<String, ConfigAttribute> loadDataSource() {
    26. Map<String, ConfigAttribute> map = new ConcurrentHashMap<>();
    27. List<UmsResource> resourceList = adminService.getResourceList();
    28. for (UmsResource resource : resourceList) {
    29. map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
    30. }
    31. return map;
    32. }
    33. };
    34. }
    35. }

     

    效果测试

    • 接下来启动我们的示例项目mall-tiny-security,使用如下账号密码登录,该账号只配置了访问/brand/listAll的权限,访问地址:http://localhost:8088/swagger-ui/

    • 然后把返回的token放入到Swagger的认证头中;

     

    • 当我们访问有权限的接口时可以正常获取到数据;

     

    • 当我们访问没有权限的接口时,返回没有访问权限的接口提示。

     

    总结

    Spring Security的升级用法确实够优雅,够简单,而且对之前用法的兼容性也比较好!个人感觉一个成熟的框架不太会在升级过程中大改用法,即使改了也会对之前的用法做兼容,所以对于绝大多数框架来说旧版本会用,新版本照样会用!

     

     

  • 相关阅读:
    WebSocket学习笔记
    国产 87235系列USB平均功率探头
    如何管理和维护组件库?
    第09讲:Java 线程优化 偏向锁,轻量级锁、重量级锁
    微信小程序和APP:关于跳转及调用支持方式整理
    CNN经典架构
    【STM32】入门(七):I2C硬件控制方式
    安卓依赖冲突问题
    transformers - 预测中间词
    电脑中病毒了一直下载安装软件怎么办?
  • 原文地址:https://blog.csdn.net/m0_67698950/article/details/125376716