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

server/2024/10/16 4:28:18/

安全可以说是公司的红线了,一般项目都会有严格的认证和授权操作,在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接口,内容如下:

java">@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: userpassword: 123456role: admin

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

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

3.添加用户

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

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

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

java">@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected 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中的另一个方法,代码如下:

java">@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected 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");}@Overrideprotected 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中添加如下接口进行测试:

java">@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提示。

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

java">@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 {@Overridepublic 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 {@Overridepublic 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.注销登录配置

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

java">@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的多次扩展,代码如下:

java">@Configuration
public class MultiHttpSecurityConfig {@BeanPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Autowiredprotected 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 {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.antMatcher("/admin/**").authorizeRequests().anyRequest().hasRole("ADMIN");}}@Configurationpublic static class OtherSecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected 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注解开启基于注解的安全配置:

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

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

java">@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中的方法进行测试。

java">@RestController
public class HelloController {@Autowiredprivate 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.设计实体

角色实体

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

用户实体

java">
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;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {ArrayList<SimpleGrantedAuthority> authorities = new ArrayList<>();for (Role role : roles) {authorities.add(new SimpleGrantedAuthority(role.getName()));}return authorities;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return locked;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return enabled;}//省略getter和setter
}

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

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

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

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

3.创建UserService

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

java">@Service
public class UserService implements UserDetailsService {@AutowiredUserMapper userMapper;@Overridepublic 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进行配置,代码如下:

java">
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService);}@Overrideprotected 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方法来确定一个请求需要哪些角色。

代码如下:

java">@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {@AutowiredMenuMapper menuMapper;// AntPathMatcher 是一个正则匹配工具,主要用来实现ant风格的URL匹配AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {//该方法的参数是一个FilterInvocation,开发者可以从FilterInvocation中提取出当前请求的URLString requestUrl = ((FilterInvocation) object).getFullRequestUrl();//从数据库中获取所有的资源信息,即menu表以及menu所对应的roleList<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");}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {//getAllConfigAttributes方法用来返回所有定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确,如果不需要校验,那么该方法直接返回null即可。return null;}@Overridepublic boolean supports(Class<?> clazz) {//返回类对象是否支持校验。return FilterInvocation.class.isAssignableFrom(clazz);}
}

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

3. 自定义AccessDecisionManager

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

java">@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {@Overridepublic 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("权限不足,请联系管理员!");}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}

4.配置过滤链

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

java">@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;@BeanPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService);}@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception {// 任何请求需要身份认证httpSecurity.authorizeRequests()// withObjectPostProcessor 允许你通过 ObjectPostProcessor 对象来修改即将被使用的安全对象。.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {// ObjectPostProcessor 接口有一个 postProcess 方法,该方法会在对象被添加到过滤器链之前被调用。@Overridepublic <O extends FilterSecurityInterceptor> O postProcess(O object) {object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource());object.setAccessDecisionManager(customAccessDecisionManager());return object;}}).and().formLogin().loginProcessingUrl("/login").permitAll().and().csrf().disable();}@Beanpublic CustomAccessDecisionManager customAccessDecisionManager() {return new CustomAccessDecisionManager();}@Beanpublic 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来存储这些信息,意味着上下文始终可用在同一执行线程中的方法。例如,获取有关当前用户的信息的方法:

java">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提供的一个基类,确保过滤器在一个请求中只执行一次。

java">
public class CustomFilter extends OncePerRequestFilter {private final AntPathRequestMatcher matcher = new AntPathRequestMatcher("/custom/**");@Overrideprotected 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 接口。

java">public class CustomFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {// 初始化逻辑}@Overridepublic 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);}@Overridepublic void destroy() {// 销毁逻辑}
}
(2)添加到过滤器链中

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

java">@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)// 其他配置...}
}

http://www.ppmy.cn/server/126849.html

相关文章

AQS原理(AbstractQueuedSynchronizer)

本篇为 [并发与多线程系列] 的第四篇&#xff0c;对应Java知识体系脑图中的 并发与多线程 模块。 这一系列将对Java中并发与多线程的内容来展开。 AQS原理&#xff08;AbstractQueuedSynchronizer&#xff09; AQS原理&#xff08;AbstractQueuedSynchronizer&#xff09;AQS整…

Python知识点:如何使用Raspberry Pi与Python进行边缘计算

开篇&#xff0c;先说一个好消息&#xff0c;截止到2025年1月1日前&#xff0c;翻到文末找到我&#xff0c;赠送定制版的开题报告和任务书&#xff0c;先到先得&#xff01;过期不候&#xff01; 如何使用Raspberry Pi与Python进行边缘计算 Raspberry Pi是一款广受欢迎的小型单…

移动端的每日任务,golang后端数据库应该怎么设计

推荐学习文档 golang应用级os框架&#xff0c;欢迎stargolang应用级os框架使用案例&#xff0c;欢迎star案例&#xff1a;基于golang开发的一款超有个性的旅游计划app经历golang实战大纲golang优秀开发常用开源库汇总想学习更多golang知识&#xff0c;这里有免费的golang学习笔…

491. 递增子序列

文章目录 491. 递增子序列思路回溯三部曲总结 491. 递增子序列 491. 递增子序列 给你一个整数数组 nums &#xff0c;找出并返回所有该数组中不同的递增子序列&#xff0c;递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。 数组中可能含有重复元素&#xff0c;如…

std::map

std::map是C标准库中的一个关联容器&#xff0c;它基于红黑树实现&#xff0c;用于存储键值对。与标准数组或向量不同&#xff0c;std::map允许你根据键来快速检索、插入和删除元素。正如std::vector包含在< vector >头文件中&#xff0c;std::map包含在< map >头文…

【Swift官方文档】7.Swift集合类型

集合类型 使用数组、集合和字典来组织数据。Swift 提供了三种主要的集合类型&#xff1a;数组、集合和字典&#xff0c;用于存储值的集合。数组是有序的值集合。集合是无序的唯一值集合。字典是无序的键值对集合。 Swift 中的数组、集合和字典始终清晰地指明它们可以存储的值…

Javascript数组研究03_手写实现_fill_filter_find_findIndex_findLast_findLastIndex

6 Array.fill() 6.1 基本介绍 fill() 方法用一个固定值填充一个数组中从起始索引&#xff08;默认为 0&#xff09;到终止索引&#xff08;默认为 array.length&#xff09;内的全部元素。它返回修改后的数组。 fill(value) fill(value, start) fill(value, start, end)输入…

实时语音交互,打造更加智能便捷的应用

随着人工智能和自然语言处理技术的进步&#xff0c;用户对智能化和便捷化应用的需求不断增加。语音交互技术以其直观的语音指令&#xff0c;革新了传统的手动输入方式&#xff0c;简化了用户操作&#xff0c;让应用变得更加易用和高效。 通过语音交互&#xff0c;用户可以在不…