当前位置: 首页 > article >正文

基于RBAC的通用权限管理系统的详细分析与实现(实现篇-Spring Security安全管理框架)

安全可以说是公司的红线了,一般项目都会有严格的认证和授权操作,在Java开发领域常见的安全框架有Shiro和Spring Security。

Shiro是一个轻量级的安全管理框架,提供了认证、授权、会话管理、密码管理、缓存管理等功能。

Spring Security是一个相对复杂的安全管理框架,功能比Shiro更加强大,权限控制细粒度更高,对OAuth 2的支持也更友好,又因为Spring Security源自Spring家族,因此可以和Spring框架无缝整合,特别是Spring Boot中提供的自动化配置方案,可以让Spring Security的使用更加便捷。

一、初步使用

(一)基本使用

1.创建项目、添加依赖

创建一个Spring Boot Web项目,然后添加spring-boot-starter-security依赖即可,代码如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

接下来在项目中添加一个简单的/hello接口,内容如下:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String  hello(){
        return "Hello World";
    }
}

接下来启动项目,启动成功后,访问/hello接口会自动跳转到登录页面,这个登录页面是由Spring Security提供的。

在这里插入图片描述
默认的用户名是user,默认的登录密码则在每次启动项目时随机生成,查看项目启动日志
在这里插入图片描述
从项目启动日志中可以看到默认的登录密码,登录成功后,用户就可以访问“/hello”接口了。
在这里插入图片描述

该方式产生的用户名和密码在每次应用启动后生效,并打印在控制台中,用户名和密码存储在内存中,再次启动后会生成新的用户名和密码。

对于只需要简单做权限控制的应用来说,这样就够了。

只用引入个spring security的maven包就做到了权限控制,简单,快捷!

2.配置用户名和密码

但是如果每次启动应用,用户名和密码都会变化,这样对开发者来说太不方便,我需要设置一个用户名和密码。

可以在application.yaml中配置默认的用户名、密码以及用户角色,配置方式如下:

spring:
  security:
    user:
      name: user
      password: 123456
      role: admin

当开发者在application.yaml中配置了默认的用户名和密码后,再次启动项目,项目启动日志就不会打印出随机生成的密码了,用户可直接使用配置好的用户名和密码登录,登录成功后,用户还具有一个角色——admin。

如果该项目不是复杂的软件应用,开发者只用引入依赖并设置下账号密码,到目前也就够了。这样难道不香吗?

3.添加用户

目前系统的用户账号只有一个,我们可以添加更多用户账号。

开发者可以自定义类继承自WebSecurityConfigurerAdapter,进而实现对Spring Security更多的自定义配置。

为简单起见,我们目前是基于内存的认证,配置方式如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin")
                .password(passwordEncoder().encode("123456"))
                .roles("ADMIN", "USER")
                .and()
                .withUser("sang")
                .password(passwordEncoder().encode("123456"))
                .roles("USER");
    }
}

自定义MyWebSecurityConfig继承自WebSecurityConfigurerAdapter,并重写configure(AuthenticationManagerBuilder auth)方法。

在该方法中配置两个用户,一个用户名是admin,密码123456,具备两个角色ADMIN和USER;另一个用户名是sang,密码是123456,具备一个角色USER。

系统采用BCryptPasswordEncoder加密,这是Springboot比较常用的加密方式。

配置完成后,重启Spring Boot项目,就可以使用这里配置的两个用户进行登录了。

4.保护接口资源

虽然现在可以实现认证功能,但是受保护的资源都是默认的,而且也不能根据实际情况进行角色管理。

如果要实现这些功能,就需要重写WebSecurityConfigurerAdapter中的另一个方法,代码如下:

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	//首先配置了三个用户,root用户具备ADMIN和DBA的角色,admin用户具备ADMIN和USER的角色,sang用户具备USER的角色。
        auth.inMemoryAuthentication()
                .withUser("root").password(passwordEncoder().encode("123456")).roles("ADMIN", "DBA")
                .and()
                .withUser("user").password(passwordEncoder().encode("123456")).roles("ADMIN", "USER")
                .and()
                .withUser("sang").password(passwordEncoder().encode("123456")).roles("USER");
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        //调用authorizeRequests()方法开启HttpSecurity的配置
        httpSecurity.authorizeRequests()
        		//必须具备ADMIN的角色
                .antMatchers("/admin/**").hasRole("ADMIN")
                //必须具备ADMIN或USER的角色
                .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
                //必须具备ADMIN和DBA的角色
                .antMatchers("/db/**").access("hasRole('ADMIN') AND hasRole('DBA')")
                //表示除了前面定义的URL模式之外,用户访问其他的URL都必须认证后访问(登录后访问)
                .anyRequest()
                .authenticated()
                .and()
                //开启表单登录,即一开始看到的登录页面               
                .formLogin()
                 //同时配置了登录接口为“/login”​,即可以直接调用“/login”接口,发起一个POST请求进行登录
                 //登录参数中用户名必须命名为username,密码必须命名为password,配置loginProcessingUrl接口主要是方便Ajax或者移动端调用登录接口。
                .loginProcessingUrl("/login")
                //表示和登录相关的接口都不需要认证即可访问。
                .permitAll()
                .and()
                // 一般接口调用都跨域名或端口了,所以要禁用CSRF保护
                .csrf()
                .disable();
    }
}

在Spring Security中,configure(HttpSecurity http) 方法是用于配置Web安全设置的关键方法。它允许开发者自定义HTTP安全策略,比如哪些URL路径需要认证、使用哪种认证机制、如何处理登录请求等。

代码中必须开启表单登录formLogin()的配置,否则用户将无法通过西戎默认提供的HTTP表单来登录系统。在目前学习到的技术中,应用程序中依赖于这种方式让用户登录,如果删除这部分代码后,用户将找不到登录入口。

除了permitAllaccess这些方法外,Security还提供了更多的权限控制方式,其他方法及说明如表所示:

在这里插入图片描述

配置完成后,接下来在Controller中添加如下接口进行测试:

@RestController
public class HelloController {
    @GetMapping("/admin/hello")
    public String admin() {
        return "hello admin!";
    }

    @GetMapping("/user/hello")
    public String user() {
        return "hello user!";
    }

    @GetMapping("/db/hello")
    public String dba() {
        return "hello dba!";
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

根据上面的配置,实现的权限控制效果如下:

  • ​“/db/hello”路径则只有root用户具有访问权限。
  • ​“/admin/hello”接口root和admin用户具有访问权限。
  • “/user/hello”接口root、admin和sang用户具有访问权限。
  • ​“/hello”路径则任何登录后的用户具有访问权限。

如图,使用普通用户sang的账号访问后台管理页面,返回403无权限。
在这里插入图片描述
可以自己通过浏览器访问尝试各种效果。

5.跳转页面改为返回json

迄今为止,登录表单一直使用Spring Security提供的页面,登录成功后也是默认的页面跳转,但是,如今前后端分离是企业级应用开发的主流,在前后端分离的开发方式中,前后端的数据交互通过JSON进行,这时,登录成功后就不是页面跳转了,而是一段JSON提示。

要实现这些功能,只需要继续完善上文的配置,代码改写如下:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    // 任何请求需要身份认证
    httpSecurity.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
            .antMatchers("/db/**").access("hasRole('ADMIN') AND hasRole('DBA')")
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .loginPage("/login_page")
            .loginProcessingUrl("/login")
            .usernameParameter("username")
            .passwordParameter("password")
            .successHandler(new MyAuthenticationSuccessHandler())
            .failureHandler(new MyAuthenticationFailureHandler())
            .permitAll()
            .and()
            .csrf()
            .disable();
}
static class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

   @Override
   public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
       response.setContentType("application/json;charset=utf-8");
       Map<String, Object> jsonResponse = new HashMap<>();
       jsonResponse.put("status", "success");
       jsonResponse.put("message", "登录成功");
       jsonResponse.put("username", authentication.getName());
       ObjectMapper objectMapper = new ObjectMapper();
       response.getWriter().write(objectMapper.writeValueAsString(jsonResponse));
   }
}

static class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

   @Override
   public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
       response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
       response.setContentType("application/json;charset=utf-8");
       Map<String, Object> jsonResponse = new HashMap<>();
       jsonResponse.put("status", "error");
       jsonResponse.put("message", "登录失败: " + e.getMessage());
       ObjectMapper objectMapper = new ObjectMapper();
       response.getWriter().write(objectMapper.writeValueAsString(jsonResponse));
   }
}

实际上上述代码,就是定义了登陆成功和登录失败的逻辑。

用户登录成功后可以跳转到某一个页面,也可以返回一段JSON,这个要看具体业务逻辑,本案例假设是第二种,用户登录成功后,返回一段登录成功的JSON。onAuthenticationSuccess方法的第三个参数一般用来获取当前登录用户的信息,在登录成功后,可以获取当前登录用户的信息一起返回给客户端。

登录失败的处理逻辑,和登录成功类似,不同的是,登录失败的回调方法里有一个AuthenticationException参数,通过这个异常参数可以获取登录失败的原因,进而给用户一个明确的提示。

其实,上面2个业务处理逻辑也不用显示继承接口,也可以构造接口的匿名实现类放在方法参数中。

在这里插入图片描述
完成后,现在通过浏览器是无法使用默认的登录页面进行跳转了,所以我们使用postman发送接口请求进行登录测试。可以看到,现在成功改成返回json数据了。

6.注销登录配置

如果想要注销登录,也只需要提供简单的配置即可,代码如下:

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    // 省略前面的配置...
    httpSecurity.logout()
            //清除身份认证信息,默认为true,表示清除。
            .logoutUrl("/logout")
            //表示是否使Session失效,默认为true。
            .invalidateHttpSession(true)
            //清除身份认证信息,默认为true,表示清除。
            .clearAuthentication(true)
            .logoutSuccessHandler((request, response, authentication) -> response.sendRedirect("/login_page"))
            .logoutSuccessUrl("/login_page")
            //清除cookie
            .deleteCookies("JSESSIONID");
}

开发者可以在logoutSuccessHandler这里处理注销成功后的业务逻辑,例如返回一段JSON提示或者跳转到登录页面等。

7.配置多个HttpSecurity

如果业务比较复杂,开发者也可以配置多个HttpSecurity,实现对WebSecurityConfigurerAdapter的多次扩展,代码如下:

@Configuration
public class MultiHttpSecurityConfig {
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN", "USER").and()
                .withUser("sang").password("123").roles("USER");
    }

    @Configuration
    @Order(1)
    public static class AdminSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("ADMIN");
        }
    }

    @Configuration
    public static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginProcessingUrl("/login")
                    .permitAll()
                    .and()
                    .csrf()
                    .disable();
        }
    }
}

这段代码定义了一个多HTTP安全配置的Spring Security设置,它包括两个不同的安全配置类:一个针对管理员路径(/admin/**),另一个针对其他所有路径。

8.方法级别的权限控制

上文介绍的认证与授权都是基于URL的,开发者也可以通过注解来灵活地配置方法安全,要使用相关注解,首先要通过@EnableGlobalMethodSecurity注解开启基于注解的安全配置:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {}
  • prePostEnabled=true会解锁@PreAuthorize@PostAuthorize两个注解,顾名思义,@PreAuthorize注解会在方法执行前进行验证,而@PostAuthorize注解在方法执行后进行验证,使用不多。
  • securedEnabled=true会解锁@Secured注解。

开启注解安全配置后,接下来创建一个MethodService进行测试,代码如下:

@Service
public class MethodService {
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "hello admin";
    }

    @PreAuthorize("hasRole('ADMIN')and hasRole('DBA')")
    public String dba() {
        return "hello dba";
    }

    @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")
    public String user() {
        return "user";
    }
}
  • @Secured("ROLE_ADMIN")注解表示访问该方法需要ADMIN角色,注意这里需要在角色前加一个前缀“ROLE_”​(非强制)。此注解是Spring Security提供的一个较早的注解,只能指定角色,不支持基于表达式的语法。
  • @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")注解表示访问该方法既需要ADMIN角色又需要DBA角色。
  • @PreAuthorize("hasAnyRole('ADMIN','DBA','USER')")表示访问该方法需要ADMIN、DBA或USER角色。
  • @PreAuthorize和@PostAuthorize中都可以使用基于表达式的语法。

最后,在Controller中注入Service并调用Service中的方法进行测试。

@RestController
public class HelloController {
    @Autowired
    private MethodService methodService;
    @GetMapping("/admin/hello")
    public String admin() {
        return methodService.admin();
    }

    @GetMapping("/user/hello")
    public String user() {
        return methodService.user();
    }

    @GetMapping("/db/hello")
    public String dba() {
        return methodService.dba();
    }

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

这里比较简单,可以自行测试。比如,登录sang账号访问/admin/hello的接口就被拦截了。
在这里插入图片描述

(二)基于数据库的认证

上面介绍的认证数据都是定义在内存中的,在真实项目中,用户的基本信息以及角色等都存储在数据库中,因此需要从数据库中获取数据进行认证。

1.设计数据库表

首先需要设计一个基本的用户角色表,一共三张表,分别是用户表、角色表以及用户角色关联表。

在这里插入图片描述

为了方便测试,可以预置几条测试数据。

因为重点是讲解spring security,需要自行引入操作数据库的依赖。这里我用的是mybaits-plus。

2.设计实体

角色实体

public class Role {
    private Long id;
    private String code;
    private String name;
}

用户实体


public class User implements UserDetails{
    private Long id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    User(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
    //省略getter和setter
}

用户实体类需要实现UserDetails接口,并实现该接口中的7个方法。如表所示:
在这里插入图片描述用户根据实际情况设置这7个方法的返回值。

因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可。

例如,getPassword()方法返回的密码和用户输入的登录密码不匹配,会自动抛出BadCredentialsException异常,isAccountNonExpired()方法返回了false,会自动抛出AccountExpiredException异常,因此对开发者而言,只需要按照数据库中的数据在这里返回相应的配置即可。本案例因为数据库中只有enabled和locked字段,故账户未过期和密码未过期两个方法都返回true。

getAuthorities()方法用来获取当前用户所具有的角色信息,本案例中,用户所具有的角色存储在roles属性中,因此该方法直接遍历roles属性,然后构造SimpleGrantedAuthority集合并返回。

3.创建UserService

接下来创建UserService,代码如下:

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user=userMapper.loadUserByUsername(username);
        if(user ==null)  {
            throw new UsernameNotFoundException("账户不存在!");            
        }
        user.setRoles(userMapper.getUserRolesByUid(user.getId()));
        return user;
    }
}

定义UserService实现UserDetailsService接口,并实现该接口中的loadUserByUsername方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,如果没有查找到用户,就抛出一个账户不存在的异常,如果查找到了用户,就继续查找该用户所具有的角色信息,并将获取到的user对象返回,再由系统提供的DaoAuthenticationProvider类去比对密码是否正确。

loadUserByUsername方法将在用户登录时自动调用。

4.配置Spring Security

接下来对Spring Security进行配置,代码如下:


@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 任何请求需要身份认证
        httpSecurity.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .antMatchers("/db/**").hasRole("DBA")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }
}

这里大部分配置和前面介绍的一致,唯一不同的是没有配置内存用户,而是将刚刚创建好的UserService配置到AuthenticationManagerBuilder中。

配置完成后,接下来就可以创建Controller进行测试了,可自行尝试。到此就实现了基于数据库的权限验证了!

(三)动态配置权限

使用HttpSecurity配置的认证授权规则还是不够灵活,无法实现资源和角色之间的动态调整,要实现动态配置URL权限,就需要开发者自定义权限配置。

1.数据库设计

这里的数据库在之前的基础上再增加一张资源表和资源角色关联表,如图所示。

在这里插入图片描述
资源表中定义了用户能够访问的URL模式,资源角色表则定义了访问该模式的URL需要什么样的角色。

在这里插入图片描述

2.自定义FilterInvocationSecurityMetadataSource

FilterInvocationSecurityMetadataSource 是 Spring Security 框架中的一个接口,它在基于过滤器的安全机制中扮演着重要的角色。Spring Security 使用这个接口来决定哪些 URL 需要进行安全检查以及这些 URL 应该被赋予什么样的访问权限。

FilterInvocationSecurityMetadataSource过滤器调用安全元数据源)的中文译名比较绕口,意味着这是一个提供给过滤器使用的来源,它为每次请求提供必要的安全相关信息,如该请求应该由哪些角色或权限来访问等。

要实现动态配置权限,首先要自定义FilterInvocationSecurityMetadataSource,Spring Security中通过FilterInvocationSecurityMetadataSource接口中的getAttributes方法来确定一个请求需要哪些角色。

代码如下:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    MenuMapper menuMapper;

    // AntPathMatcher 是一个正则匹配工具,主要用来实现ant风格的URL匹配
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
    //该方法的参数是一个FilterInvocation,开发者可以从FilterInvocation中提取出当前请求的URL
        String requestUrl = ((FilterInvocation) object).getFullRequestUrl();
        //从数据库中获取所有的资源信息,即menu表以及menu所对应的role
        List<Menu> menus = menuMapper.getAllMenus();
        //遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。
        for (Menu menu : menus) {
            if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
                List<Role> roles = menu.getRoles();
                String[] str = new String[roles.size()];
                for (int i = 0; i < roles.size(); i++) {
                    str[i] = roles.get(i).getName();
                }
                //获取匹配的URL所需的角色名数组后,转换为Collection<ConfigAttribute>返回
                return SecurityConfig.createList(str);
            }
        }
        //如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回ROLE_LOGIN。
        // 没有匹配上的,只要登录之后就可以访问,这里“ROLE_LOGIN”只是一个标记,有待进一步处理。
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
    	//getAllConfigAttributes方法用来返回所有定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确,如果不需要校验,那么该方法直接返回null即可。
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
   		//返回类对象是否支持校验。
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

FilterInvocationSecurityMetadataSource接口的默认实现类是DefaultFilterInvocationSecurityMetadataSource,参考DefaultFilterInvocationSecurityMetadataSource的实现,开发者可以定义自己的FilterInvocationSecurityMetadataSource

3. 自定义AccessDecisionManager

当一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,接下来就会来到AccessDecisionManager类中进行角色信息的比对,自定义AccessDecisionManager如下:

@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new AccessDeniedException("尚未登录,请登录!");
                } else {
                    return;
                }
            }
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足,请联系管理员!");
    }

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

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

4.配置过滤链

最后,在Spring Security中配置如上两个自定义类,部分源码如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 任何请求需要身份认证
        httpSecurity.authorizeRequests()
         		// withObjectPostProcessor 允许你通过 ObjectPostProcessor 对象来修改即将被使用的安全对象。
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                // ObjectPostProcessor 接口有一个 postProcess 方法,该方法会在对象被添加到过滤器链之前被调用。
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource());
                        object.setAccessDecisionManager(customAccessDecisionManager());
                        return object;
                    }
                })
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

    @Bean
    public CustomAccessDecisionManager customAccessDecisionManager() {
        return new CustomAccessDecisionManager();
    }

    @Bean
    public CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource() {
        return new CustomFilterInvocationSecurityMetadataSource();
    }

}


本代码主要是修改了WebSecurityConfig类中的configure(HttpSecurity http)方法的实现并添加了两个Bean。

在定义FilterSecurityInterceptor时,将我们自定义的两个实例设置进去即可。

经过上面的配置,我们已经实现了基于RBAC的动态配置权限,权限和资源的关系可以在menu_role表中动态调整。

二、Security安全管理框架

上面的代码只是做到了权限控制的基本使用,但是实际生产环境需要满足更复杂的需求,比如验证码、JWT框架等。

接下来,先了解下security的核心组件,便于我们对权限控制做进一步改造。

(一)Security的核心组件

Spring Security最核心的功能是认证和授权,主要依赖一系列的组件和过滤器相互配合来完成。Spring Security的核心组件包括SecurityContextHolder、Authentication、AuthenticationManager、UserDetailsService、UserDetails等。

1.SecurityContextHolder

SecurityContextHolder用于存储应用程序安全上下文(Spring Context)的详细信息,如当前操作的用户对象信息、认证状态、角色权限信息等。

默认情况下,SecurityContextHolder使用ThreadLocal来存储这些信息,意味着上下文始终可用在同一执行线程中的方法。例如,获取有关当前用户的信息的方法:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
    String username = ((UserDetails)principal).getUsername();
} else {
    String username = principal.toString();
}

因为身份信息与线程是绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。例如,获取当前经过身份验证的用户的名称,其中getAuthentication()返回认证信息,getPrincipal()返回身份信息,UserDetails是对用户信息的封装类。

2.Authentication

Authentication是认证信息接口,集成了Principal类。该接口定义了如表所示的方法。
在这里插入图片描述
Authentication定义了getAuthorities()、getCredentials()、getDetails()和getPrincipal()等接口实现认证功能。

principal:特征,能唯一标识用户身份的属性,一个主题(用户)可以有多个principal;
举个例子:你去登录一些网站时可以用用户名,也可以用手机或邮箱,这些principal是别人可以知道的;
credential:凭证,主题(用户)才知道的。
举个例子:你给手机开锁,可以使用屏幕密码也可以使用人脸识别,屏幕密码和人脸是你个人(用户)才拥有的。
最常见的 principal 和 credential 组合就是用户名 / 密码了。

3.AuthenticationManager

AuthenticationManager认证管理器负责验证。

认证成功后,AuthenticationManager返回填充了用户认证信息(包括权限信息、身份信息、详细信息等,但密码通常会被移除)的Authentication实例,然后将Authentication设置到SecurityContextHolder容器中。

AuthenticationManager接口是认证相关的核心接口,也是发起认证的入口。但它一般不直接认证,其常用实现类ProviderManager内部会维护一个List<AuthenticationProvider>列表,其中存放了多种认证方式,默认情况下,只需要通过一个AuthenticationProvider的认证就可以被认为登录成功。

4.UserDetails

UserDetails用户信息接口定义最详细的用户信息。该接口中的方法如表所示。
在这里插入图片描述

5.UserDetailsService

UserDetailsService负责从特定的地方加载用户信息,通常通过JdbcDaoImpl从数据库加载具体实现,也可以通过内存映射InMemoryDaoImpl具体实现。

当用户尝试登录时,Spring Security 会使用 AuthenticationManager 来处理认证请求。在这个过程中,UserDetailsService 被用来加载用户详细信息,以便进行身份验证和授权。

默认情况下,Spring Security 可以使用内存中的用户数据(例如通过 inMemoryAuthentication() 配置)。但在实际应用中,用户数据通常存储在数据库、LDAP 或其他外部系统中。通过实现 UserDetailsService,你可以从这些数据源中加载用户数据。

(二)Security验证流程

1.验证流程

Security看起来很复杂,其实一句话就能概述:一组过滤器链组成的权限认证流程。

Security采用的是责任链的设计模式,它有一条很长的过滤器链,整个过滤器链的执行流程如图所示。

在这里插入图片描述
Security本质就是通过一组过滤器来过滤HTTP请求,将HTTP请求转发到不同的处理模块,最后经过业务逻辑处理返回Response的过程。

Security默认的过滤器的入口在HttpSecurity对象中,那么HttpSecurity是如何加载的呢?

HttpSecurity对象实际提供的是各个过滤器对应的配置类,通过配置类来控制对应过滤器属性的配置,最后将过滤器加载到HttpSecurity的过滤链中。

HttpSecurity提供的默认过滤器及其配置类如表所示。

在这里插入图片描述

表中提供的默认过滤器并不是在HttpSecurity对象初始化的时候就全部加载的,而是根据用户定制情况进行加载。

同时,Security提供了多种登录认证的方式,由多种过滤器共同实现,不同的过滤器被加载到应用中,我们可以根据不同的需求自定义登录认证配置。

2.过滤器

重要过滤器如下表所示:

重要过滤器释义
HttpSessionContextIntegrationFilter位于过滤器顶端,第一个起作用的过滤器。
用途一,在执行其他过滤器之前,率先判断用户的session中是否已经存在一个SecurityContext了。如果存在,就把SecurityContext拿出来,放到SecurityContextHolder中,供Spring Security的其他部分使用。如果不存在,就创建一个SecurityContext出来,还是放到SecurityContextHolder中,供Spring Security的其他部分使用。
用途二,在所有过滤器执行完毕后,清空SecurityContextHolder,因为SecurityContextHolder是基于ThreadLocal的,如果在操作完成后清空ThreadLocal,会受到服务器的线程池机制的影响。
AuthenticationProcessingFilter处理form登陆的过滤器,与form登陆有关的所有操作都是在此进行的。
默认情况下只处理/j_spring_security_check请求,这个请求应该是用户使用form登陆后的提交地址此过滤器执行的基本操作时,通过用户名和密码判断用户是否有效,如果登录成功就跳转到成功页面(可能是登陆之前访问的受保护页面,也可能是默认的成功页面),如果登录失败,就跳转到失败页面。
BasicProcessingFilter此过滤器用于进行basic验证,功能与AuthenticationProcessingFilter类似,只是验证的方式不同。
AnonymousProcessingFilter为了保证操作统一性,当用户没有登陆时,默认为用户分配匿名用户的权限。
ExceptionTranslationFilter此过滤器的作用是处理中FilterSecurityInterceptor抛出的异常,然后将请求重定向到对应页面,或返回对应的响应错误代码
FilterSecurityInterceptor用户的权限控制都包含在这个过滤器中。
功能一:如果用户尚未登陆,则抛出AuthenticationCredentialsNotFoundException“尚未认证异常”。
功能二:如果用户已登录,但是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。
功能三:如果用户已登录,也具有访问当前资源的权限,则放行。我们可以通过配置方式来自定义拦截规则
UsernamePasswordAuthenticationFilter实现了其父类AbstractAuthenticationProcessingFilter中的attemptAuthentication方法。
这个方法会调用认证管理器AuthenticationManager去认证。
(1)自定义过滤器

如果你需要自定义过滤器,通常有两种方式:

继承 OncePerRequestFilter:这是最常见的方法。OncePerRequestFilter 是Spring Security提供的一个基类,确保过滤器在一个请求中只执行一次。


public class CustomFilter extends OncePerRequestFilter {

    private final AntPathRequestMatcher matcher = new AntPathRequestMatcher("/custom/**");

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (matcher.matches(request)) {
            // 自定义逻辑
            System.out.println("CustomFilter is executing for: " + request.getRequestURI());
        }
        filterChain.doFilter(request, response);
    }
}

实现 Filter 接口:如果不需要 OncePerRequestFilter 提供的功能,可以直接实现 javax.servlet.Filter 接口。

public class CustomFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 初始化逻辑
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 自定义逻辑
        System.out.println("CustomFilter is executing for: " + httpRequest.getRequestURI());

        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
        // 销毁逻辑
    }
}
(2)添加到过滤器链中

无论你选择哪种方式实现自定义过滤器,都需要在Spring Security配置中将其添加到过滤器链中。你可以通过继承 WebSecurityConfigurerAdapter 并重写 addFilterBefore 或 addFilterAfter 方法来实现这一点。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)
            // 其他配置...
    }
}

http://www.kler.cn/news/330625.html

相关文章:

  • 如何避免我的住宅ip被污染
  • 解决方案:梯度提升树(Gradient Boosting Trees)跟GBDT(Gradient Boosting Decision Trees)有什么区别
  • 已经部署了ssl证书,网站仍被Chrome标记为不安全怎么办?
  • golang grpc初体验
  • OpenEuler配置本地yum源
  • 排序算法之快速排序
  • 【Qt】控件概述 (1)
  • MySQL 分组
  • 完美解决Idea中如何对Java Agent进行断点调试的方式
  • 动态规划
  • Stream流的中间方法
  • 本地生活服务项目有哪些:如何利用本地生活市场,打开线下流量!
  • oracle 定时任务每月27号到月底
  • 信息安全工程师(13)网络攻击一般过程
  • 【分布式微服务云原生】Docker常用命令指南
  • 【预备理论知识——1】深度学习:概率论概述
  • Redis入门第五步:Redis持久化
  • 什么是“0day漏洞”?
  • 【leetcode】 45.跳跃游戏 ||
  • 如何快速自定义一个Spring Boot Starter!!