权限管理

权限管理实现了对用户访问系统的控制 按照 安全规则 或 安全策略 控制用户跨域访问,而且只能访问自己被授权的资源

包括两部分(先进行身份认证,认证通过后用户具有该资源的访问权限)

  • 身份认证:就是判断⼀个⽤户是否为合法⽤户的处理过程
  • 授权:即访问控制,控制谁能访问哪些资源

整体架构

认证和授权是分开的,⽆论使⽤什么样的认证⽅式,都不会影响授权。这是两个独⽴的存在,这种独⽴带来的好处之⼀,就是可以⾮常⽅便地整合⼀些外部的解决⽅案

image-20221026221848631

认证

AuthenticationManager

在SpringSecurity中认证是由AuthenticationManager接口进行负责的,接口定义为:

image-20221026224356299
  • 返回Authentication表示认证成功
  • 返回AuthenticationException异常,表示认证失败
image-20221026225148440

AuthenticationManager 主要实现类为 ProviderManager,在 ProviderManager 中管理了众多 AuthenticationProvider 实例。

在⼀次完整的认证流程中,Spring Security 允许存在多个 AuthenticationProvider ,⽤来实现多种认证⽅式,这些 AuthenticationProvider 都是由 ProviderManager 进⾏统⼀管理的


Authentication

认证成功后的信息主要是由该类的实现类进行保存

image-20221026225912349
  • getAuthorities 获取⽤户权限信息
  • getCredentials 获取⽤户凭证信息,⼀般指密码
  • getDetails 获取⽤户详细信息
  • getPrincipal 获取⽤户身份信息,⽤户名、⽤户对象等
  • isAuthenticated ⽤户是否认证成功

SecurityContextHolder

SecurityContextHolder ⽤来获取登录之后⽤户信息。SpringSecurity 会将登录⽤户数据保存在 Session 中

  1. 当⽤户登录成功后,SpringSecurity 会将登录成功的⽤户信息保存到 SecurityContextHolder 中
  2. SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使⽤ ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改
  3. 当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空
  4. 每当有请求到来时,SpringSecurity 就会先从 Session 中取出⽤户登录数据,保存到 SecurityContextHolder 中,⽅便在该请求的后续处理过程中使⽤
  5. 在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这⼀策略非常方便⽤户在 Controller、Service 层以及任何代码中获取当前登录⽤户数据

授权

AccessDecisionManager和AccessDecisionVoter都有众多实现类,在AccessDecisionManager中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问

AccessDecisionManager

访问决策管理器,用来决定此次访问是否被允许

image-20221026233528330

AccessDecisionVoter

访问决定投票器,投票器会检查用户是否具备应有的角色,进而投出赞成,反对或弃权票

image-20221026233732854

ConfigAttribute

用来保存授权时的角色信息

image-20221026234155943

SpringSecurity中,用户请求资源时,需要的角色会被封装成一个ConfigAttribute对象

在ConfigAttribute中只有一个getAttribute()方法,该方法会返回一个String字符串(即为角色的名称)

角色名称都带有一个 ROLE 前缀,投票器AccessDecisionVoter所做的事情其实就是:比较用户具有的各个角色和请求资源所需的ConfigAttribute之间的关系


整合springboot

依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

定义两个Controller,在访问localhost:8080/hellolocalhost:8080/index的时候分别打印出hello security与hello index

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class IndexController {
@RequestMapping("/index")
public String hello() {
System.out.println("hello index");
return "hello index";
}
}

@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
System.out.println("hello security");
return "hello security";
}
}

当引入Spring Security后没有进行任何配置,所有请求都会跳转到localhost:8080/login

这其中关系到SpringBootWebSecurityConfiguration

这个类是SpringBoot的自动配置类,通过源码得知,默认情况下对所有请求进行权限控制

这也就是在引入SpringSecurity后,没有进行任何配置,请求都会被拦截的原因

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
@ConditionalOnDefaultWebSecurity
@ConditionalOnWebApplication(type = Type.SERVLET)
class SpringBootWebSecurityConfiguration {
@Bean
@Order(SecurityProperties.BASIC_AUTH_ORDER)
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 默认对http里面的所有请求进行拦截,只有进行认证后才可以进行访问(支持表单认证或httpBasic认证)
http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
return http.build();
}
}

默认的生效条件

默认情况下,条件都满足。,SpringSecurity核心配置都在WebSecurityConfigurerAdapter类中,我们一般通过重写该类,来对SpringSecurity实现功能的增强

1
2
3
4
5
6
7
8
9
10
11
12
13
class DefaultWebSecurityCondition extends AllNestedConditions {
DefaultWebSecurityCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
//@ConditionalOnClass:当classpath下能找到指定类才条件匹配
@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
static class Classes {
}
//@ConditionalOnMissingBean:没有在当前项目自定义指定类时,条件匹配
@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
static class Beans {
}
}

原理

在 Spring Security 中认证、授权 等功能都是基于过滤器完成的 官方文档

image-20221028010613009

SpringSecurity的Servlet包含在FilterChainProxy中。FilterChainProxy是SpringSecurity提供的一个特殊的Filter,SpringSecurity通过SecurityFilterChain委托许多Filter实例

FilterChainProxy是一个Bean,它通常被包裹在DelegatingFilterProxy中


image-20221028010403757

SecurityFilterChainFilterChainProxy调用,然后FilterChainProxy再调用Spring的一些安全过滤器

FilterChainProxy可以用来确定应该使用哪个SecurityFilterChain。这允许为不同的环境提供一个完全独立的配置


image-20221028010422897

在出现多个SecurityFilterChain中,FilterChainProxy决定应该使用哪个SecurityFilterChain,只有第一个匹配的SecurityFilterChain才会被调用

  • 请求的URL为/api/messages/,它将首先与SecurityFilterChain0的/api/**相匹配,所以只有SecurityFilterChain0会被调用,尽管它也与另外一个匹配。

  • 请求的URL/messages/的URL,它将不会与SecurityFilterChain0的/api/**模式相匹配,所以FilterChainProxy将继续尝试每个SecurityFilterChain。假设没有其他的SecurityFilterChain实例与之匹配,就会调用SecurityFilterChainn

每个SecurityFilterChain都可以是唯一的,并且是单独配置的


Security Filters一览

过滤器 过滤器作用 默认是否加载
ChannelProcessingFilter 过滤请求协议 HTTP 、HTTPS NO
WebAsyncManagerIntegrationFilter 将 WebAsyncManger 与 SpringSecurity 上下文进行集成 YES
SecurityContextPersistenceFilter 在处理请求之前,将安全信息加载到 SecurityContextHolder 中 YES
HeaderWriterFilter 处理头信息加入响应中 YES
CorsFilter 处理跨域问题 NO
CsrfFilter 处理 CSRF 攻击 YES
LogoutFilter 处理注销登录 YES
OAuth2AuthorizationRequestRedirectFilter 处理 OAuth2 认证重定向 NO
Saml2WebSsoAuthenticationRequestFilter 处理 SAML 认证 NO
X509AuthenticationFilter 处理 X509 认证 NO
AbstractPreAuthenticatedProcessingFilter 处理预认证问题 NO
CasAuthenticationFilter 处理 CAS 单点登录 NO
OAuth2LoginAuthenticationFilter 处理 OAuth2 认证 NO
Saml2WebSsoAuthenticationFilter 处理 SAML 认证 NO
UsernamePasswordAuthenticationFilter 处理表单登录 YES
OpenIDAuthenticationFilter 处理 OpenID 认证 NO
DefaultLoginPageGeneratingFilter 配置默认登录页面 YES
DefaultLogoutPageGeneratingFilter 配置默认注销页面 YES
ConcurrentSessionFilter 处理 Session 有效期 NO
DigestAuthenticationFilter 处理 HTTP 摘要认证 NO
BearerTokenAuthenticationFilter 处理 OAuth2 认证的 Access Token NO
BasicAuthenticationFilter 处理 HttpBasic 登录 YES
RequestCacheAwareFilter 处理请求缓存 YES
SecurityContextHolder<br />AwareRequestFilter 包装原始请求 YES
JaasApiIntegrationFilter 处理 JAAS 认证 NO
RememberMeAuthenticationFilter 处理 RememberMe 登录 NO
AnonymousAuthenticationFilter 配置匿名认证 YES
OAuth2AuthorizationCodeGrantFilter 处理OAuth2认证中授权码 NO
SessionManagementFilter 处理 session 并发问题 YES
ExceptionTranslationFilter 处理认证/授权中的异常 YES
FilterSecurityInterceptor 处理授权相关 YES
SwitchUserFilter 处理账户切换 NO

自定义认证

自定义资源权限规则

当存在公共资源和受限资源时,为了方便资源进行访问与管理,我们需要自定义资源权限规则

image-20221027212841357

注意:

  • permitAll() 代表放⾏该资源,⽆需认证和授权可以直接访问

  • anyRequest().authenticated() 代表所有请求,必须认证之后才能访问

  • formLogin() 代表开启表单认证


可以通过继承WebSecurityConfigurerAdapter,重写configure方法来进行自定义资源权限认证

image-20221028140916669

现在该方式已被弃用,可以使用自定义一个filterChain的Bean来实现自定义资源权限认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index", "/login.html")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
return http.build();
}
}

自定义登录界面

1、引入thymeleaf

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<dependency>

2、定义controller

1
2
3
4
5
6
7
@Controller
public class LoginController {
@RequestMapping("/login.html")
public String login() {
return "login";
}
}

3、定义登录界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户登录</title>
</head>
<body>
<h1>用户登录</h1>
<form method="post" th:action="@{/doLogin}">
用户名:<input name="username" type="text"><br>
密码:<input name="password" type="password"><br>
<input type="submit" value="登录">
</form>
</body>
</html>
  • 登录表单 method 必须为 post

  • action 的请求路径为 /doLogin

  • ⽤户名的 name 属性为 uname

  • 密码的 name 属性为 passwd

注意:

  • successForwardUrl是forward转发(始终在认证成功之后跳转到指定请求,不会跳转到之前请求路径)
  • defaultSuccessUrl是redirect重定向(根据上一条保存请求进行成功跳转,可以传入第二个参数进行修改)
  • 二者只能选择一个进行配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index", "/login.html")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html") //指定默认的登录页面 注意:一旦自定义登录界面以后,必须指定登录的url
.loginProcessingUrl("/doLogin")
.usernameParameter("uanme") //自定义请求的参数
.passwordParameter("passwd") //自定义请求的参数
.successForwardUrl("/index") //认证成功后 forward 跳转路径
//.defaultSuccessUrl("/index", true) //默认认证成功后跳转的路径
.and()
.csrf().disable(); //禁止 csrf 跨站请求保护
return http.build();
}
}

自定义登录成功处理

在认证成功后,不进行页面跳转,而是给前端返回一个JSON通知登录成功与否

image-20221028145548469

根据接⼝的描述信息,得知登录成功会自动回调这个方法,successForwardUrl、defaultSuccessUrl也是由它的⼦类实现的

image-20221028145445544

自定义AuthenticationSuccessHandler实现

  • 在自定义资源权限规则中,设置successHandler()
image-20221028150913501
  • 实现AuthenticationSuccessHandler

在重写方法中配置返回前端的数据和ContentType

1
2
3
4
5
6
7
8
9
10
11
12
public class SuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> res = new HashMap<>();
res.put("msg", "登录成功");
res.put("status", 200);
res.put("authentication", authentication);
response.setContentType("application/json;charset=UTF-8");
String str = new ObjectMapper().writeValueAsString(res);
response.getWriter().println(str);
}
}
image-20221028150738870

显示登录失败信息

Spring Security 在登录失败之后会将异常信息存储到 request 、session作⽤域中 key 为SPRING_SECURITY_LAST_EXCEPTION 命名属性中

image-20221028154258963

  • failureUrl 失败以后的重定向跳转
  • failureForwardUrl 失败以后的 forward 跳转
  • 注意:如果要获取 request 中异常信息,这⾥只能使⽤failureForwardUrl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigurer {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.mvcMatchers("/index", "/login.html")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/login.html") //指定默认的登录页面 注意:一旦自定义登录界面以后,必须指定登录的url
.loginProcessingUrl("/doLogin")
.successHandler(new SuccessHandler()) //认证成功时,返回给前端数据,而不是跳转页面
.failureForwardUrl("/login.html") //认证失败之后的 forward 跳转
// .failureUrl("/login.html") //认证失败之后的 redirect 跳转(默认)
.and()
.csrf().disable(); //禁止 csrf 跨站请求保护
return http.build();
}
}
image-20221028154037687 image-20221028154432922

自定义登录失败处理

1、和自定义登录成功处理类似,在自定义资源权限规则中,设置failureHandler()

image-20221028160221565

2、实现AuthenticationFailureHandler,在重写方法中配置返回前端的数据和ContentType

1
2
3
4
5
6
7
8
9
10
11
public class FailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
HashMap<String, Object> res = new HashMap<>();
res.put("msg", "登录失败:" + exception.getMessage());
res.put("status", 500);
response.setContentType("application/json;charset=UTF-8");
String str = new ObjectMapper().writeValueAsString(res);
response.getWriter().println(str);
}
}

注销登录

SpringSecurity中提供了默认的注销登录的配置,LogoutFilter过滤器专门用来处理注销登录的相关请求

1、注销成功后跳转页面

  • invalidateHttpSession:退出时是否是 session 失效,默认值为 true

  • clearAuthentication:退出时是否清除认证信息,默认值为 true

  • logoutSuccessUrl:退出登录时跳转地址

  • logoutRequestMatcher:配置多个注销登录请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class WebSecurityConfigurer {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
//......
/**
* 指定多个注销登录的url(可以指定请求方式)
* OrRequestMatcher: 表示任意一个满足都可以
* AndRequestMatcher: 表示两个必须同时满足
*/
.logoutRequestMatcher(new OrRequestMatcher(
new AntPathRequestMatcher("/logout-1", "GET"),
new AntPathRequestMatcher("/logout-2", "POST")
))
// .invalidateHttpSession(true) //默认会话失效(默认)
// .clearAuthentication(true) //清除当前认证标记(默认)
.logoutSuccessUrl("/login.html") //指定注销成功后,跳转的页面
.and()
.csrf().disable(); //禁止 csrf 跨站请求保护
return http.build();
}
}

按照上面配置之后,当访问localhost:8080/logout-1后,界面将会跳转到登录界面

2、注销成功后返回信息

  • 与上面类似登录成功或失败后返回前端信息类似,都是通过实现LogoutSuccessHandler类来进行
1
2
3
4
5
6
7
8
9
10
11
public class LogoutHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap<String, Object> res = new HashMap<>();
res.put("msg", "注销成功"+authentication);
res.put("status", 200);
response.setContentType("application/json;charset=UTF-8");
String str = new ObjectMapper().writeValueAsString(res);
response.getWriter().println(str);
}
}
  • 在自定义资源权限管理器做以下修改
image-20221028164920136 image-20221028165146860

获取登录用户数据

SecurityContextHolder ⽤来获取登录之后用户信息。SpringSecurity 会将登录用户数据保存在 Session 中

image-20221030213726378

在SpringSecurity中使用策略设计模式来进行数据的获取与存储

image-20221030213939759
  • MODE_THREADLOCAL:这种存放策略是将 SecurityContext 存放在 ThreadLocal 中,⼤家知道 Threadlocal 的特点是在哪个线程中存储就要在哪个线程中读取,这其 实⾮常适合 web 应⽤,因为在默认情况下,⼀个请求⽆论经过多少 Filter 到达 Servlet,都是由⼀个线程来处理的。这也是 SecurityContextHolder 的默认存储 策略,这种存储策略意味着如果在具体的业务处理代码中,开启了⼦线程,在⼦线程中 去获取登录⽤户数据,就会获取不到

  • MODE_INHERITABLETHREADLOCAL:这种存储模式适⽤于多线程环境,如果希望在子线程中也能够获取到登录⽤户数据,那么可以使用这种存储模式

  • MODE_GLOBAL:这种存储模式实际上是将数据保存在⼀个静态变量中,这种模式很少使⽤到

可以通过增加 VM Options 参数进行修改

1
如:-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

SecurityContextHolderStrategy 接⼝⽤来定义存储策略方法

1
2
3
4
5
6
public interface SecurityContextHolderStrategy {
void clearContext();
SecurityContext getContext();
void setContext(SecurityContext context);
SecurityContext createEmptyContext();
}
  • clearContext:该⽅法⽤来清除存储的 SecurityContext对象
  • getContext:该⽅法⽤来获取存储的 SecurityContext 对象
  • setContext:该⽅法⽤来设置存储的 SecurityContext 对象
  • createEmptyContext:该⽅法则⽤来创建⼀个空的 SecurityContext 对象

页面上获取用户信息

  • 引入依赖
1
2
3
4
5
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
  • 页面加入命名空间
1
2
<html lang="en" xmlns:th="https:!"www.thymeleaf.org"
xmlns:sec="http:!"www.thymeleaf.org/extras/spring-security">
  • 在页面中使用
1
2
3
4
5
6
7
8
<!--获取认证⽤户名-->
<ul>
<li sec:authentication="principal.username"></li>
<li sec:authentication="principal.authorities"></li>
<li sec:authentication="principal.accountNonExpired"></li>
<li sec:authentication="principal.accountNonLocked"></li>
<li sec:authentication="principal.credentialsNonExpired"></li>
</ul>

⾃定义认证数据源

认证流程分析

参考文档-docs.spring.io

image-20221030221850350

1、当发起一个认证用户的请求时,会被UsernamePasswordAuthenticationFilter给拦截到,在这个Filter中,会从HttpServletRequest中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken

2、认证被传递到AuthenticationManager中进行认证

3、如果认证失败

  • 清除SecurityContextHodler
  • 清除记住我中的信息
  • 回调 AuthenticationFailureHandler 处理

如果认证成功

  • 将认证信息存储到 SecurityContextHodler
  • 调⽤记住我中的功能
  • 回调 AuthenticationSuccessHandler 处理

注意:

AuthenticationManager 是认证的核⼼类,但实际上在底层真正认证时还离不开 ProviderManager 以及 AuthenticationProvider

  • AuthenticationManager 是一个认证管理器,它实现了SpringSecurity 过滤器要执行的认证操作
  • ProviderManagerAuthenticationManager 接口的实现类,SpringSecurity 认证时默认使用
  • AuthenticationProvider 就是针对不同的身份类型执行的具体的身份认证
image-20221030231516937
  • ProviderManagerAuthenticationManager 的唯⼀实现,默认情况下 AuthenticationManager 就是⼀ 个 ProviderManager 

ProviderManager与AuthenticationProvider

image-20221030232037461

SpringSecurity 允许系统同时支持多种不同的认证方式(如:同时支持用户名密码验证,Remember认证,手机号码动态认证)

  • 不同的认证方式对应了不同的AuthenticationProvider,所以一个完整的认证流程可能由多个AuthenticationProvider来提供

  • 多个AuthenticationProvider组成一个列表,然后由ProviderManager代理

  • ProviderManager本身还可以配置一个AuthenticationManager作为parent,在ProviderManager认证失败后,可以进入到parent再次认证(ProviderManager也可作为parent)

  • ProviderManager本身也可以有多个,多个ProviderManager共用一个parent,这样就可以实现采用不同的请求路径实现不同的认证规则

  • 参考文档-docs.spring.io

image-20221031163805182

配置全局 AuthenticationManager

默认的AuthenticationManager

找当前项⽬中是否存在自定义 UserDetailService 实例,自动将当前项目 UserDetailService 实例设置为数据源

1
2
3
4
5
6
7
@Configuration
public class WebSecurityConfigurer{
@Autowired
public void initialize(AuthenticationManagerBuilder builder){
//builder......
}
}

进行自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//自定义AuthenticationManager
@Bean
AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception {
AuthenticationManager authenticationManager = httpSecurity.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService()).and().build();
return authenticationManager;
}

@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("aaa").password("{noop}123").roles("admin").build());
return userDetailsService;
}

自定义内存数据源


自定义数据库数据源


Remember Me


会话管理


CSRF


跨域


异常处理


授权


OAuth2与JWT


实战案例