1. SpringSecurity的核心原理
-
对于最原始Servlet请求处理的层次结构
客户端->过滤器链->Servlet
-
对于在SpringMVC中处理请求的层次结构 如何让Filter与Spring建立连接呢? 因此它增加了一个
DelegatingFilterProxy
它是SpringMVC提供的的Filter,它内部代理了一个原生的Filter的Spring的Bean对象 它还有一个好处是,它内部代理的Filter可以进行延迟加载,并不需要立即加载这个FilterBean
- 对于在SpringSecurity中处理请求的层次结构 如何让SpringSecurity与与SpringMVC建立连接呢? 因此,SpringSecurity增加了一个
FilterChainProxy
,它也是一个Filter,而FilterChainProxy
正是在SpringMVC 的DelegatingFilterProxy
这个Filter类中代理的FilterChainProxy
Filter对象,这样就建立了连接
而对于SpringSecurity,它自身提供的FilterChainProxy
中,它又包含了SecurityFilterChain
,俗称安全的过滤器链
- 对于
SecurityFilterChain
,该对象一般都是Bean对象,它内部包含了N
个过滤器,并且可以通过RequestMacther
请求匹配器基于请求的任何内容来确定 这些过滤器是否会被调用
-
对于
FilterChainProxy
,它并不是只有一个SecurityFilterChain
,内部可以有1-N
个,每一个SecurityFilterChain
都负责自己本身的职责 例如:
SecurityFilterChain A负责处理表达式为/a
请求,内部的过滤器只是处理/a
使用的过滤器
SecurityFilterChain B负责处理表达式为/**
请求,内部的过滤器只是处理/**
使用的过滤器
这样,在FilterChainProxy
中,它会遍历所有的SecurityFilterChain
,只要有一个SecurityFilterChain
请求匹配器可以处理,则调用SecurityFilterChain内部的过滤器进行校验
-
到此,Security的运行大致流程基本完成,那到底有哪些过滤器在工作呢?官方提供了一个类
FilterOrderRegistration
,它记录了大概30个左右的过滤器
1.1. SpringSecurity认证的体系结构
-
SecurityContextHolder - SecurityContextHolder是Spring Security存储身份验证详细信息的地方
-
SecurityContext - 从SecurityContextHolder中获得,并包含当前已验证用户的身份验证
-
Authentication - 可以是AuthenticationManager的输入,以提供用户提供的用于身份验证的凭据,也可以是SecurityContext中的当前用户
- principal: 标识用户当使用用户名/密码进行身份验证时,这通常是 UserDetails.
- credentials: 通常是密码在许多情况下,在用户通过身份验证后清除该属性,以确保它不会泄露
- authorities: GrantedAuthority实例是授予用户的高级权限两个例子是role和scope .principal:
-
GrantedAuthority - 在身份验证上授予主体的权限(即角色、范围等)
-
AuthenticationManager - 定义Spring Security的过滤器如何执行身份验证的API
- AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API
- 然后由调用AuthenticationManager的控制器(即Spring Security的Filters实例)在SecurityContextHolder上设置返回的身份验证
- 如果你没有集成Spring Security的Filters实例,你可以直接设置SecurityContextHolder,而不需要使用AuthenticationManager
- 虽然AuthenticationManager的实现可以是任何东西,但最常见的实现是ProviderManager
-
ProviderManager - AuthenticationManager最常见的实现
- ProviderManager是AuthenticationManager最常用的实现
ProviderManager委托给AuthenticationProvider实例列表,每个AuthenticationProvider都有机会表明身份验证应该是成功的、失败的,或者表明它不能做出决定,并允许下游的AuthenticationProvider做出决定 - 如果配置的AuthenticationProvider实例都不能进行身份验证,则身份验证失败并产生一个ProviderNotFoundException
- 这是一个特殊的AuthenticationException,表明ProviderManager没有配置支持传递给它的身份验证类型
- ProviderManager是AuthenticationManager最常用的实现
* 上图可以发现,每一个AuthenticationProvider都是执行特定类型的身份验证,例如验证用户名密码,RememberMe
对于不同的ProviderManager实例,也可能共享同一个父AuthenticationManager这个在存在多个SecurityFilterChain实例的场景比较常见
这个ProviderManager有一些共同的身份认证共享父AuthenticationManager,但是他们也各自具有自身的身份认证机制
- AuthenticationProvider - 由ProviderManager用于执行特定类型的身份验证
- 我们可以有多个AuthenticationProvider注入到ProviderManager中,每个AuthenticationProvider执行特定类型的身份验证
- 例如: DaoAuthenticationProvider支持用户名/密码,而JwtAuthenticationProvider支持JWT令牌的身份验证
- AuthenticationEntryPoint - 认证的入口点,用于从客户端请求凭证(例如,重定向到登录页面,发送WWW-Authenticate响应等)
- AbstractAuthenticationProcessingFilter - 用于身份验证的基本过滤器,这还可以很好地了解高层次的身份验证流以及各个部分如何协同工作
- AbstractAuthenticationProcessingFilter 用作验证用户凭据的基本筛选器
- 在对凭证进行身份验证之前,SpringSecurity通常通过使用AuthenticationEntryPoint请求凭证,接下来,AbstractAuthenticationProcessingFilter可以对提交给它的任何身份验证请求进行身份验证
- 当用户提交他们的凭据时,AbstractAuthenticationProcessingFilter从HttpServletRequest创建一个要进行身份验证
创建的身份验证Authentication类型取决于AbstractAuthenticationProcessingFilter的子类
例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的用户名和密码中创建UsernamePasswordAuthenticationToken - 接下来,将身份验证传递到AuthenticationManager中进行身份验证
- 如果身份验证失败则处理Failure逻辑
- 清除SecurityContextHolder
- RememberMeServices调用loginFail,如果remember-me没有配置,这是一个no-op(无操作)
- 调用AuthenticationFailureHandler,回调onAuthenticationFailure方法,做认证失败的逻辑处理,在这里可以给客户端响应JSON
- 如果身份验证成功,执行Success操作
- SessionAuthenticationStrategy收到新登录的通知,执行SessionAuthenticationStrategy接口回调
- 身份验证设置在securitycontexholder上,如果您需要保存SecurityContext以便在将来的请求中自动设置它
则必须显式调用SecurityContextRepository#saveContext - RememberMeServices。调用loginSuccess,如果没有配置remember-me,这是一个no-op
- ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件
- 调用AuthenticationSuccessHandler。参见AuthenticationSuccessHandler接口
- UserDetailsService - 加载用户详细信息的的Service服务接口,用来加载用户信息,然后进行身份认证
- PasswordEncoder - 密码编码器,在做身份认证的情况下,需要将给定的密码加密与UserDetailsService加载出来的用户信息做比对
1.1.1 认证的用户名密码配置
-
密码的存储策略
- 内存实现
// 直接设置内存中的编码 @Bean public UserDetailsService users() { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); } // 使用密码编码器进行编码 @Bean public UserDetailsService users() { // The builder will ensure the passwords are encoded before saving in memory UserBuilder users = User.withDefaultPasswordEncoder(); UserDetails user = users .username("user") .password("password") .roles("USER") .build(); UserDetails admin = users .username("admin") .password("password") .roles("USER", "ADMIN") .build(); return new InMemoryUserDetailsManager(user, admin); }
- 数据库JDBC实现
- JdbcUserDetailsManager存在数据库脚本
- 需要配置数据源
@Bean UserDetailsManager users(DataSource dataSource) { UserDetails user = User.builder() .username("user") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER") .build(); UserDetails admin = User.builder() .username("admin") .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") .roles("USER", "ADMIN") .build(); JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); users.createUser(user); users.createUser(admin); return users; }
- 自定义的实现UserDetailsService
-
实现UserDetailsService接口,该接口是提供给AuthenticationProvider进行工作的
-
AuthenticationProvider可以设置UserDetailsService和PasswordEncoder,从而进行身份验证
-
由于内部存在默认的AuthenticationProvider,会找UserDetailsService和PasswordEncoder的Bean,因此我们只需要配置这两个Bean即可,计算没有配置,也有默认的
-
如果需要自己暴露一个AuthenticationProvider的Bean也是可以的
-
工作原理AuthenticationProvider
- 来自读取用户名和密码部分的身份验证过滤器将UsernamePasswordAuthenticationToken传递给AuthenticationManager该AuthenticationManager由ProviderManager实现
- ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider, DaoAuthenticationProvider从UserDetailsService中查找UserDetails用户详细信息
- DaoAuthenticationProvider使用PasswordEncoder在上一步返回的UserDetails上验证密码。
- 当身份验证成功时,返回的身份验证类型为UsernamePasswordAuthenticationToken,其主体是配置好的UserDetailsService返回的UserDetails
- 最终,返回的UsernamePasswordAuthenticationToken由身份验证过滤器在SecurityContextHolder上设置。
-
-
当要通过RestApi调用登录接口的时候,需要SpringSecurity做身份认证,这个时候,我们需要暴露一个AuthenticationManager的Bean,在Api接口中使用认证管理器进行认证
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
// 注册一个认证管理器,两种方式
// 方式一(比较推荐)
// AuthenticationConfiguration: 自动配置的类,该类会提供一个获取AuthenticationManager的方法
// @Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) {
return configuration.getAuthenticationManager();
}
// 方式二
@Bean
public AuthenticationManager authenticationManager(UserDetailsService userDetailsService,PasswordEncoder passwordEncoder) {
// 身份认证的提供者,实际干活的类,ProviderManager代理了多个AuthenticationProvider
// 每一个AuthenticationProvider都会进行工作,直到认证成功
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
// 自定义的加载用户信息的服务
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
// 返回AuthenticationManager的实现
ProviderManager providerManager = new ProviderManager(authenticationProvider);
// 禁用缓存,这些根据自己的条件来设置
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
// 有三种实现,一种是InMemoryUserDetailsManager,一种是JdbcUserDetailsManager,还有完全自定义
@Bean
public UserDetailsService userDetailsService() {
// 基于内存的实现,内部维护了多个用户列表,只需要从维护的用户列表中作比对
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user").password("password").roles("USER").build();
return new InMemoryUserDetailsManager(userDetails);
// 基于JDBC的实现,会自动创建用户+权限两张表,从该表获取数据进行校验
return new JdbcUserDetailsManager(dataSource())
// 自定义实现
return new UserDetailsService(){
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 该用户必须使用UserDetails接口
Users user = usersMapper.selectOne(new LambdaQueryWrapper<Users>().eq(Users::getUsername, username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 通过角色字符串转换为GrantedAuthority信息
// tip: 在SpringSecurity中,角色权限都是都用授权对象表示GrantedAuthority
// 如果是角色,那么角色名称必须以ROLE_开头,如果不是ROLE_开头,则为权限
// 因此,如何需要封装角色和权限一起,就需要对权限添加ROLE_前缀再转为GrantedAuthority,权限则直接转换为GrantedAuthority
Collection<Users.LuckGrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()).stream().map(m -> new Users.LuckGrantedAuthority(m.getAuthority())).collect(Collectors.toList());
user.setAuthorities(authorities);
// 也可以返回SpringSecurity提供的内置的UserDetails对象
return user;
}
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@RestController
public class LoginController {
private final AuthenticationManager authenticationManager;
public LoginController(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest) {
Authentication authenticationRequest =
UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.username(), loginRequest.password());
Authentication authenticationResponse =
this.authenticationManager.authenticate(authenticationRequest);
}
}
1.1.2 表单登录(默认开启)
1.1.2.1 表单登录是如何工作的
- 首先,用户向未经授权的资源(/private)发出未经身份验证的请求。
- Spring Security的AuthorizationFilter表示通过抛出AccessDeniedException拒绝未经身份验证的请求。
- 由于用户没有经过身份验证,ExceptionTranslationFilter启动认证,并使用配置的AuthenticationEntryPoint发送重定向到登录页面。在大多数情况下,AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint的一个实例
- 浏览器请求重定向到的登录页面
1.1.2.2 当提交表单登录的时候做了什么
-
UsernamePasswordAuthenticationFilter拦截到请求,UsernamePasswordAuthenticationFilter继承了 AbstractAuthenticationProcessingFilter,所以它们执行的流程图基本一样
-
当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter创建一个UsernamePasswordAuthenticationToken,这是一种认证类型,通过从HttpServletRequestl实例中提取用户名和密码。
-
接下来,UsernamePasswordAuthenticationToken被传递到需要认证的AuthenticationManager实例中。
AuthenticationManager的认证细节取决于用户信息的存储方式(有基于内存,数据库,自定义的实现,UserDetailsService) -
如果认证失败,则执行failure逻辑
- 清除SecurityContextHolder。
- 调用RememberMeServices.loginFail,如果remember-me没有配置,这是一个no-op,不做任何事
- 调用AuthenticationFailureHandler接口处理
-
如果认证通过,则执行成功逻辑
- SessionAuthenticationStrategy接口收到新登录的通知
- 身份验证信息设置在 SecurityContextHolder上,由SecurityContextPersistenceFilter类完成
- 调用RememberMeServices.loginSuccess,如果remember-me没有配置,这是一个no-op,不做任何事
- ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent事件
- 调用AuthenticationSuccessHandler,通常这是一个SimpleUrAuthenticationsuccesHandler,会重定向到指定页面,当我们重定向到登录页面时,它会重定向到由ExceptionTranslationfiter保存的请求,因为ExceptionTranslationfiter保存了要重定向的请求对象,如果不要这个处理方式,我们可以自定义这个接口
1.1.2.3 自定义登录表单页面
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
// 登录表单的参数
.usernameParameter("username")
.passwordParameter("password")
// 校验失败之后访问的地址,默认的地址为/login并且携带error参数
.failureUrl("/login.html?error")
// 当登录认证成功之后自定义处理的逻辑,这是自定义的逻辑,有默认的认证成功逻辑
.successHandler(authenticationSuccessHandler)
// 当登录认证失败之后的自定义处理的逻辑
.failureHandler(authenticationFailureHandler);
.permitAll()
);
}
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}
-
有几个关键点需要注意(这些到可以进行配置)
-
表单的请求路径为POST的/login
-
表单需要包含一个CSRF令牌,Thymeleaf会自动包含
-
表单应该为用户名指定参数为username,密码为password,记住我为remember-me
-
如果找到名为error的请求参数,表示用户名密码错误
-
如果找到名为logout的请求参数,则表示退出成功
-
1.1.3 请求之间SecurityContext的持久化策略
- 在Security中,请求之间SecurityContext的持久化策略是由SecurityContextRepository处理的
- 默认的实现为DelegatingSecurityContextRepository,而DelegatingSecurityContextRepository默认代理了两个真正工作的SecurityContextRepository
- HttpSessionSecurityContextRepository
- RequestAttributeSecurityContextRepository
- 其中还有一个实现NullSecurityContextRepository,它什么都不做,会导致SecurityContext为空
// 自定义SecurityContext的持久化策略
// 默认为DelegatingSecurityContextRepository repository = new DelegatingSecurityContextRepository(new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository());
// 持久化到request和session域中
// NullSecurityContextRepository不做任何操作,这回导致SecurityContext为空,验证失败
http.securityContext(ctx -> {
// SecurityContextRepository repository = new NullSecurityContextRepository();
SecurityContextRepository repository = new DelegatingSecurityContextRepository(new RequestAttributeSecurityContextRepository(), new HttpSessionSecurityContextRepository());
ctx.securityContextRepository(repository);
});
- 而SecurityContextRepository工作的地方有两个
-
SecurityContextPersistenceFilter
-
SecurityContextHolderFilter
-
1.1.4 Session会话管理
1.1.4.1 使用什么可能会用到会话管理
- 想限制用户可以并发登录的次数(客户端同时登录)
- 自己存储以及删除身份验证信息,而不是让SpringSecurity来为我做这件事情
1.1.4.2 Session管理提供的组件
SessionManagementFilter
- SessionManagementFilter根据SecurityContextRepository的内容来决定当前用户是否通过了身份认证,因为认证的信息是持久化到SecurityContextRepository中,一般是通过非交互式身份验证机制完成的,例如remember-me
- 如果SecurityContextRepository包含SecurityContext,那表示用户已经通过了身份认证,直接放行
- 如果没有,获取SecurityContextHolder本地线程中是否存在非匿名的认证信息,因为我们可以通过自定义的过滤器来给SecurityContextHolder来设置我们自己认证的信息
- 此时本地的SecurityContextHolder存在认证信息,表示被其他过滤器认证过,那么调用SessionAuthenticationStrategy实现来对该认证信息进行认证操作
- 如果本地的SecurityContextHolder也不存在认证信息,那么表示用户没有认证,此时额外做一件事,就是判断该Session有没有过期,如果该Session过期,那么调用InvalidSessionStrategy的实现来完成Session失效的逻辑处理,一般来说,最常见的行为就是重定向到一个固定的URL,默认的实现为SimpleRedirectInvalidSessionStrategy,如果SimpleRedirectInvalidSessionStrategy配置的URL无效,执行InvalidSessionStrategy操作
1.1.4.3 Session相关配置
-
如果想自己实现身份认证的情况下,还需要在不同会话,请求中实现Authentication认证信息的共享,通过SecurityContextHolder本地的线程共享式无法完成夸会话的,因此我们可以进行自定义
// 创建Session的持久化SecurityContext的策略,当然也可以自定义 private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository(); @PostMapping("/login") public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated( loginRequest.getUsername(), loginRequest.getPassword()); // 手动身份认证 Authentication authentication = authenticationManager.authenticate(token); // 创建上下文对象 SecurityContext context = securityContextHolderStrategy.createEmptyContext(); // 将认证信息上下文对象保存到持久话对象中 context.setAuthentication(authentication); securityContextHolderStrategy.setContext(context); securityContextRepository.saveContext(context, request, response); } class LoginRequest { private String username; private String password; }
-
当如果我们不需要创建Session,请求之间是无状态的,我们可以使用
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
来配置,它默认的持久化为NullSecurityContextRepository
,相反,对于无状态的请求之间,例如HttpBasic中,可以修改持久化策略,将NullSecurityContextRepository
改为HttpSessionSecurityContextRepository@Bean SecurityFilterChain web(HttpSecurity http) throws Exception { http.httpBasic((basic) -> basic .addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() { @Override public <O extends BasicAuthenticationFilter> O postProcess(O filter) { filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository()); return filter; } }) ); return http.build(); }
-
当我们使用
SessionCreationPolicy.NEVER
来配置session会话的策略时,可能仍然会创建会话,因为这种情况是请求数据保存到了Session中,所以要从session获取数据,因此才会创建,如果要禁用Session创建,我们可以使用RequestCache
来完成@Bean SecurityFilterChain springSecurity(HttpSecurity http) throws Exception { // 使用不进行请求缓存的实现,这样请求数据就不会进行任何缓存 RequestCache nullRequestCache = new NullRequestCache(); http.requestCache((cache) -> cache .requestCache(nullRequestCache) ); return http.build(); }
-
SpringSecurity5和SpringSecurity6中SecurityContext保存的区别
- 在5.x中,是通过SecurityContextPersistenceFilter完成SecurityContextRepository对上下文的自动保存操作,这个操作必须在Response和SecurityContextPersistenceFilter之前完成,可能出现一些意向不到的问题
- 因此在6.x中,将SecurityContextHolderFilter替换SecurityContextPersistenceFilter,该过滤器只会从SecurityContextRepository读取上下文,填充到本地的SecurityContextHodler中,如果需要在请求之间进行持久化,则需要手动执行SecurityContextRepository的saveContext操作
- 默认情况下,requireExplicitSave为true,使用SecurityContextHolderFilter,如果为false,则使用SecurityContextPersistenceFilter
-
Session的并发控制
- 当我们只允许一个用户同时在线的时候,我们可进行控制,这可以防止用户多次登录,每一次登录都会替换上一次的会话信息
- 当表单登录请求被拒绝的时候,会跳转到authentication-failure-url配置的url中
- 当remeber-me非交互式登录的时候,会像客户端发送401未授权错误,如果希望使用错误页面,可以配置session-authentication-error-url
- 如果不想在Session失效的时候做默认的操作,我们可以自定义InvalidSessionStrategy,完成会话失效的操作
// 配置事件发布器,确保Session监听器可以监听器事件 @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) { http.sessionManagement(session -> session.maximumSessions(1) // 用户没有注销之前,不允许再次登录 .maxSessionsPreventsLogin(true) // session实现重定向到的url .invalidSessionUrl("/invalidSession") // 自定义会话失效的策略 .invalidSessionStrategy(new MyCustomInvalidSessionStrategy()) ); return http.build(); }
- 当我们只允许一个用户同时在线的时候,我们可进行控制,这可以防止用户多次登录,每一次登录都会替换上一次的会话信息
-
当我们在注销登录的时候,我们期望删除Cookie中的JSESSIONID
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout((logout) -> logout
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
);
return http.build();
}
或者
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.logout(logout -> logout
.deleteCookies("JSESSIONID")
);
return http.build();
}
- 会话固定攻击保护
- 通过访问站点创建会话,然后盗取别人的JESSIONID信息进行登录
- Security提供了三种推荐选项
- changeSessionId ,修改JSESSIONID
- newSession,创建一个新会话,不复制当前会话信息(但是Security相关会话信息还是回复制)
- migrateSession ,创建一个新会话,合并已存在的会话信息
- none,关闭防护
- 当会话固定共计触发时,会发布SessionFixationProte ctionEvent事件,如果使用changeSessionId ,还会导致
HttpSessionIdListener
的触发
http.sessionManagement((session) ->
session.sessionFixation((sessionFixation) -> sessionFixation.newSession());
1.1.4.4 SecurityContextHolder的获取策略
- 默认情况,SecurityContextHolder可以很好的完成工作,但是,当存在多个SecurityContext的情况下,可能出现不好的结果,因为在SecurityContextHolder中,获取SecurityContext的策略是一个静态变量,也就意味着,多个SecurityContext的情况将会导致SecurityContextHolder具有竞争,可能获取到不是自己期望的上下文对象
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
-
为了解决这种问题,我们可以配置SecurityContextHolder的策略,从策略获取或者设置上下文
public class SomeClass { // 从SecurityContextHolder获取到策略,保存到当前对象中 private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy(); public void someMethod() { UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated( loginRequest.getUsername(), loginRequest.getPassword()); Authentication authentication = this.authenticationManager.authenticate(token); // 再通过securityContextHolderStrategy策略来操作上下文对象 // 因为各自持有的是各自的securityContextHolderStrategy,而不在共用SecurityContextHolder的这个策略 SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); context.setAuthentication(authentication); this.securityContextHolderStrategy.setContext(context); } }
1.1.5 Remember-Me记住我实现
1.1.5.1 “记住我”或“持久登录”概念
- 身份验证指的是web站点能够在会话之间记住主体的身份
- 这通常是通过向浏览器发送一个cookie来实现的,cookie在以后的会话中被检测到,并导致自动登录。
- SpringSecurity为这些操作提供了必要的钩子,并提供了两个具体的“记住我”实现。
- 一种方法使用散列来保护基于cookie的令牌的安全性,另一种方法使用数据库或其他持久存储机制来存储生成的令牌。
- 注意,这两个实现都需要UserDetailsService
- 如果您使用不使用UserDetailsService的身份验证提供程序(例如LDAP提供程序),则除非在应用程序上下文中也有UserDetailsService bean,否则它将无法工作。
1.1.5.2 Remember-me相关接口和实现
-
与UsernamePasswordAuthenticationFilter一起使用,并通过父类AbstractAuthenticationProcessingFilter中的successfulAuthentication或者unsuccessfulAuthentication方法在认证成功或者失败调用RememberMeServices中的loginFail或者loginSuccess方法
-
RememberMeServices接口的autoLogin自动登录方法则是在
RememberMeAuthenticationFilter
进行操作的当Security上下文中存在认证信息就进行自动登录,自动登录逻辑则获取请求中的Cookie进行解析用户信息(具体获取身份信息需要根据不同的RememberMeServices来决定),解析出正确的用户信息则将Authentication返回,最终通过认证管理器对解析好的身份信息进行认证,如果认证成功,则完成自动登录,否则认证失败 -
SpringSecurity提供了2个RememberMeServices的实现(都是基于Cookie实现)
-
TokenBasedRememberMeServices
- 该实现支持简单的哈希令牌方法,将用户信息使用SHA256(默认)进行编码,发送到客户端Cookie中
- 在自动登录的时候,则会解析Cookie数据,将数据包装成一个RemeberMeAuthenticationToken对象进行身份认证,它是一个Authentication实现,最终由RememberMeAuthenticationProvider(认证管理器的提供者AuthenticationProvider)来进行身份认证的,该RememberMeAuthenticationProvider实在开启RememberMe配置的时候设置到httpSecurity中
- 在身份验证的提供者和TokenBasedRememberMeServices之间共享秘钥,TokenBasedRememberMeServices需要UserDetailService,它可以从查询出来的用户信息进行用户名密码比对,并生成RemeberMeAuthenticationToken
- 如果被盗用,则无需认证,具有一定的被盗风险
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { http.authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) .rememberMe((remember) -> remember .rememberMeServices(rememberMeServices) ); return http.build(); } @Bean RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256; TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm); rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5); return rememberMe; }
-
PersistentTokenBasedRememberMeServices
- 基于持久化数据库的实现,数据库脚本
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
- 和TokenBasedRememberMeServices基本相似,就是多了一个series安全机制,每次自动登录都会更新这个值,当Cookie解析之后的值与数据库保存的值不一致时,抛出异常,然后再次重新登录认证
- 即使被盗用,重新认证一下被盗用的Cookie则会失效,相对安全,但是不是绝对安全
-
自定义实现使用请求头Token实现
@Component @Slf4j public class LuckRememberService implements RememberMeServices { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private ObjectMapper objectMapper; @SneakyThrows @Override public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String token = request.getHeader("token"); if (StrUtil.isBlank(token)) { return null; } boolean success = JWTUtil.verify(token, "luck".getBytes(StandardCharsets.UTF_8)); if (!success) { return null; } JWT jwt = JWTUtil.parseToken(token); NumberWithFormat userId = (NumberWithFormat) jwt.getPayload("userId"); String user = stringRedisTemplate.opsForValue().get(userId + ""); if (StrUtil.isBlank(user)) { return null; } Users users = objectMapper.readValue(user, Users.class); // 创建认证信息,token通过表示认证成功,修改SecurityContextHolder的认证标识 UsernamePasswordAuthenticationToken authenticated = UsernamePasswordAuthenticationToken.authenticated(users, null, users.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticated); return authenticated; } @Override public void loginFail(HttpServletRequest request, HttpServletResponse response) { log.error("自动登录失败"); } @Override public void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { log.error("自动登录成功"); } }
-
1.1.6 匿名身份访问
- 对于不需要进行校验的URL,可以简单的对他们设置匿名访问,实质上就是未经过认证的用户身份,但是在SecurityContextHolder存在一个匿名的认证对象
- Spring Security的匿名身份验证为您提供了一种更方便的方式来配置访问控制属性。对servlet API调用的调用,如getCallerPrincipal,仍然返回null,即使在SecurityContextHolder中实际上有一个匿名身份验证对象
- 在其他情况下,匿名身份验证也很有用,例如当审计拦截器查询SecurityContextHolder以确定哪个主体负责给定操作时。如果类知道SecurityContextHolder总是包含一个匿名的Authentication对象并且从不包含null,那么类的编写就会更加健壮。
- SpringSecurity3.0会自动提供匿名身份的支持,三个类来提供匿名身份的支持
- AnonymousAuthenticationToken: 匿名用户的认证信息对象
- AnonymousAuthenticationProvider: 匿名用户的认证管理器
- AnonymousAuthenticationFilter: 匿名用户认证的过滤器
- 逻辑上与URL拦截放行不一样,放行不会进行权限认证,但是匿名会,它是通过正常的身份认证,只是认证的方式是匿名的方式,其实功能本质上是一样的
- AuthenticationTrustResolver
- 该接口提供身份类型的判断,会在ExceptionTranslationFilter处理该接口,当抛出未认证AccessDeniedException异常的情况下,如果是匿名用户,并不是直接响应403,而是启动指定authenticationEntryPoint端点的处理机制
// 有参数解析器,会将SecurityContext中的Authentication给这个参数,名称对象则是AnonymousAuthenticationToken
@GetMapping("/")
public String method(Authentication authentication) {
if (authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
} else {
return "not anonymous";
}
}
// 通过@CurrentSecurityContext获取上下文对象,可以获取到匿名认证信息为AnonymousAuthenticationToken
@GetMapping("/")
public String method(@CurrentSecurityContext SecurityContext context) {
return context.getAuthentication().getName();
}
1.1.7 注销登录
- SpringBootStarterSecurity或使用@EnableSecurity,自动会添加Logout的支持,并且会提供一个GET的/logout和Post的/logout请求处理
- 如果请求GET /logout,将得到一个注销页面,请求POST/logout,需要一个CSRF令牌
- 注意: 如果在配置中禁用CSRF保护,则不会像用户显示注销确认的页面,则是直接执行注销
- 如果想自定义退出的url,或者配置注销成功跳转的url
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/my/success/endpoint").permitAll()
)`
http.logout((logout) -> logout.logoutUrl("/my/logout/uri")
.logoutSuccessUrl("/my/success/endpoint")
// 该请求重定向的时候不会拦截
.permitAll()
)`
-
还可以指定自己的注销的处理逻辑,为LogoutHandler接口实现
-
由于该接口是执行清理操作,所以不应该抛出异常
-
// 清理Cookie或者自定义实现 CookieClearingLogoutHandler cookies = new CookieClearingLogoutHandler("our-custom-cookie"); http.logout((logout) -> logout.addLogoutHandler(cookies)) http.logout((logout) -> logout.deleteCookies("our-custom-cookie")) HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter()); http.logout((logout) -> logout.addLogoutHandler(clearSiteData)) HeaderWriterLogoutHandler clearSiteData = new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.COOKIES)); http.logout((logout) -> logout.addLogoutHandler(clearSiteData))
-
大多数时候,注销成功使用logoutSuccessUrl跳转就可以,但是我们也可以执行注销成功之后的处理逻辑LogoutSuccessHandler
-
http.logout((logout) -> logout.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()))
- 如果像实现自己的注销登录逻辑,最好使用SecurityContextLogoutHandler,它会帮我们完成清理操作
- 此外,可能还需要根据情况清除SecurityContextHolderStrategy和SecurityContextRepository信息
// 如果SecurityContextLogoutHandler执行失败,则SecurityContext仍然可用,实际没有注销
SecurityContextLogoutHandler logoutHandler = new SecurityContextLogoutHandler();
@PostMapping("/my/logout")
public String performLogout(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
this.logoutHandler.doLogout(request, response, authentication);
return "redirect:/home";
}
1.1.8 跨域CORS处理
-
CORS是在Spring提供的支持,并且CORS必须在SpringSecurity之前处理,因为CORS会发送一个预检请求,并且不携带任何cookies,导致该请求在SpringSecurity通不过授权
-
最简单的方式是,使用CorSFilter,用户可以提供一个CorsConfigurationSource,来在SpringSecurity提供支持
- 注意: 如果有多个CorsConfigurationSource的Bean,则SpringSecurity不会自动配置CORS的支持,这个时候需要手动配置指定的CorsConfigurationSource
@Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://example.com")); configuration.setAllowedMethods(Arrays.asList("GET","POST")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors(cors->{ // 存在多个CorsConfigurationSourceBean的时候需要手动设置,只有一个不需要 cors.configurationSource(corsConfigurationSource()) }) return http.build(); } }
1.1.9 CSRF跨站请求伪造网络攻击
-
前提,在用户已经登录的情况下发起攻击
-
攻击流程: 用户在某网站登录,再访问发起攻击的网站,此时攻击的网站存在一个脚本,例如script src=攻击的url或者image src=攻击的url,这个时候,这些url就会携带cookie访问目标接口,不需要进行认证就达到攻击的效果
-
SpringSecurity中的CSRF是如何工作的
-
CSRF分为两个部分
- 通过委托给CsrfTokenRequestHandler,使CsrfToken对应用程序可用
- 确定请求是否需要SCRF保护,加载并验证令牌,并处理AccessDeniedException
-
如上图具体的工作流程
- 首先在CsrfFilter中工作,加载DeferredCsrfToken,它是一个接口,它的实现类RepositoryDeferredCsrfToken包含对CsrfTokenRepository的引用,以便可以加载持久化CsrfToken
- 其次CsrfToken通过DeferredCsrfToken创建(同时发送给客户端cookie),然后交给CsrfTokenRequestHandler处理,它负责将CsrfToken设置到请求域中,每一个请求都需要创建CsrfToken
- 主CSRF保护开始检查当前请求是否需要CSRF保护,如果不需要,继续下一个过滤器,结束CSRF处理
- 如果需要CSRF保护,则持久化CsrfToken,最终从DeferredCsrfToken中加载
- 客户端提供CSRF令牌,如果有的话,使用CsrfTokenRequestHandler解析
- 解析之后,将实际的令牌与持久化的令牌进行对比,如果有效,继续执行过滤器链
- 如果无效,或者确实,抛出AccessDeniedException异常,交给AccessDeniedHandler处理并结束
-
持久化CsrfToken
-
使用CsrfTokenRepository接口完成,默认的实现为HttpSessionCsrfTokenRepository,存在session中,还提供了CookieCsrfTokenRepository,存在Cookie中,也可以自定义实现,存在任何地方
-
使用HttpSessionCsrfTokenRepository读取csrfToken是从请求头X-CSRF-TOKEN或者请求参数_csrf获取值
-
使用
CookieCsrfTokenRepository
,cookie的名称为XSRF-TOKEN,读取csrfToken是从请求头X-CSRF-TOKEN或者请求参数_csrf获取值 -
使用自定义的CsrfTokenRepository实现
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf((csrf) -> csrf .csrfTokenRepository(new HttpSessionCsrfTokenRepository()) .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRepository(LuckCsrfTokenRepository) ); return http.build(); } @Component @Slf4j public class LuckCsrfTokenRepository implements CsrfTokenRepository { @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken("luck", "luck", "luck"); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { response.addCookie(new Cookie(token.getHeaderName(), token.getToken())); } @Override public CsrfToken loadToken(HttpServletRequest request) { String csrf = request.getHeader("luck"); if (StrUtil.isBlank(csrf)) { csrf = request.getParameter("luck"); } if (StrUtil.isBlank(csrf)) { return null; } return JSONUtil.toBean(csrf, DefaultCsrfToken.class); } }
- 默认情况下,CsrfToken是有CsrfTokenRequestHandler处理的,它负责获取参数或者请求头csrf的值(默认实现),也可以对该值进行解析,并且将该CsrfToken保存到请求域属性中进行共享
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf((csrf) -> csrf // 也可以是自定义实现 XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler(); // 设置在request中设置CsrfToken的属性名 requestHandler.setCsrfRequestAttributeName("luck"); // 也可以是自定义实现 .csrfTokenRequestHandler(requestHandler) ); return http.build(); } }
-
-
禁用Csrf或者某些请求禁用csrf
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.csrf((csrf) -> csrf // 忽略某些请求 .ignoringRequestMatchers("/api/*") // 或者禁用 .disable() ); return http.build(); }
-
Csrf对文件上传的支持
// 在SpringSecurity整合SpringMVC初始化的时候,添加MultipartFilter过滤器 public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer { @Override protected void beforeSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new MultipartFilter()); } }
-
Csrf对SpringMVC的API接口的支持
@ControllerAdvice
public class CsrfControllerAdvice {
@ModelAttribute
public void getCsrfToken(HttpServletResponse response, CsrfToken csrfToken) {
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
}
}
@RestController
public class CsrfController {
@GetMapping("/csrf")
public CsrfToken csrf(CsrfToken csrfToken) {
return csrfToken;
}
}
1.2. SpringSecurity授权的体系
1.2.1 核心类授权管理器
public interface AuthorizationManager<T> {
default void verify(Supplier<Authentication> authentication, T object) {
// 权限校验
AuthorizationDecision decision = check(authentication, object);
// 如果授权不通过,抛出异常
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
}
// 权限校验,AuthorizationDecision表示是否校验通过
@Nullable
AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}
1.2.1.1 授权管理器是基于委托实现的
核心的委托授权管理器RequestMatcherDelegatingAuthorizationManager
该类内部维护了多个[路径匹配器与AuthorizationManager]的组合,对于某一个url pattern,可以委托给对应的授权管理器进行授权
-
AuthorityAuthorizationManager
最简单的身份授权管理器,它配置了一组给定的授权,在当前身份验证中匹配最终工作还是委托AuthoritiesAuthorizationManager
实现 -
AuthoritiesAuthorizationManager
可用于区分匿名,身份验证和remember-me身份验证下进行某些有限的访问,但还是要通过登录来确认其他身份来获取所有访问权限 -
AuthorizationManagers
工具类,用静态方法组合多个授权管理器进行授权 -
自定义授权管理器,推荐实现
AuthorizationManager
,而不是使用遗留的AccessDecisionManager和AccessDecisionVoter
-
如果使用了AccessDecisionManager和AccessDecisionVoter,则可以进行适配为AuthorizationManager,然后关联到
SecurityFilterChain
中 -
@Component public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager { private final AccessDecisionManager accessDecisionManager; private final SecurityMetadataSource securityMetadataSource; @Override public AuthorizationDecision check(Supplier<Authentication> authentication, Object object) { try { Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object); this.accessDecisionManager.decide(authentication.get(), object, attributes); return new AuthorizationDecision(true); } catch (AccessDeniedException ex) { return new AuthorizationDecision(false); } } @Override public void verify(Supplier<Authentication> authentication, Object object) { Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(object); this.accessDecisionManager.decide(authentication.get(), object, attributes); } }
-
1.2.2 角色的层次结构
-
例如, 超级管理员具有管理员,普通用户的所有权限,在授权的时候自动包含具有层次结构下的所有权限,也提供了支持
@Bean static RoleHierarchy roleHierarchy() { RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); // 使用 > 来确定层次结构 hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF ROLE_STAFF > ROLE_USE ROLE_USER > ROLE_GUEST"); return hierarchy; } // 如果使用方法注解,需要添加这个表达式处理器 @Bean static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) { DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); expressionHandler.setRoleHierarchy(roleHierarchy); return expressionHandler; }
-
注意,暂时还不支持@EnableMethodSecurity注解,如果要使用这个,则需要使用@EnableGlobalMethodSecurity这个注解,但是这个注解已经过时了
授权管理器是如何工作的
-
AuthorizationFilter进行工作
-
从SecurityContextHolder中获取认证信息
-
将认证信息和Request对象传递给
AuthorizationManager
,实际上工作的AuthorizationManager都是委托给RequestMatcherDelegatingAuthorizationManager进行授权校验 -
校验失败,抛出AccessDeniedException异常,发布AuthorizationDeniedEvent事件, ,由ExceptionTranslationFilter来处理
-
授权成功,发布一个AuthorizationGrantedEvent事件,继续执行其他逻辑
-
默认情况下,授权过滤器AuthorizationFilter在所有过滤器的最后一个,因此,在它之前的过滤器都不需要进行授权,就算是自定义的过滤器如果添加到该过滤器之前,也不需要授权
-
AuthorizationFilter在每一个请求上都会执行,无论是转发,异常,include都会被要求授权,对于访问页面的跳转,就会经过两次该过滤器,一次是本身的url,一次是转发到页面的url,如果不需要这种情况发生,则需要放行这种分发类型的请求
http.authorizeHttpRequests(authorize -> // 对于所有的转发请求,不进行拦截,如果拦截,那么访问html页面的情况,会执行两次过滤器,一次是本身的url,一次是页面转发的url .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() // 对于所有的异常转发请求,不进行拦截,如果拦截,那么发生的异常会抛出给服务器,转发到/error路径也会被拦截两次 .dispatcherTypeMatchers(DispatcherType.ERROR).permitAll() // 根据请求方式来进行权限校验 .requestMatchers(HttpMethod.GET).hasAuthority("READ") .requestMatchers(HttpMethod.POST).hasAuthority("WRITE") // 其他类型的请求,都被拒绝 // .anyRequest().denyAll() );
1.2.3 多Servlet的请求的匹配规则
-
如果不仅仅只有DispatchServlet,contextPath为默认为"/“,通过spring.mvc.servlet.path修改了,还有其他的Servlet,contentPath为”/api",这两种方式,使用
requestMatchers(String)
就会有问题,按照以下方法可以完成@Bean MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { return new MvcRequestMatcher.Builder(introspector).servletPath("/api"); } @Bean SecurityFilterChain appEndpoints(HttpSecurity http, MvcRequestMatcher.Builder mvc) { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(mvc.pattern("/api/**")).hasAuthority("api") .anyRequest().authenticated() ); return http.build(); }
1.2.4 自定义匹配规则
-
只需要实现RequestMatcher,调用requestMatchers进行匹配即可
RequestMatcher printview = (request) -> request.getParameter("print") != null; http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(printview).hasAuthority("print") .anyRequest().authenticated() )
1.2.5 授权方法整理
permitAll——请求不需要授权,是一个公共端点;注意,在这种情况下,永远不会从会话中检索身份验证
denyAll -请求在任何情况下都不被允许;注意,在这种情况下,永远不会从会话中检索身份验证
hasAuthority——请求要求认证具有与给定值匹配的GrantedAuthority
hasRole - hasAuthority的快捷方式,前缀为ROLE_或任何被配置为默认前缀的东西
hasAnyAuthority——请求要求认证具有与任何给定值匹配的GrantedAuthority
hasAnyRole - hasAnyAuthority的快捷方式,它以ROLE_或任何被配置为默认前缀的前缀为前缀
access——请求使用这个自定义AuthorizationManager来确定访问
1.2.6 授权方法对SPEL表达式的支持
authentication -与此方法调用关联的authentication实例
principal—与此方法调用关联的身份验证#getPrincipal
1.2.7 其他用法
- 如果需要针对任意请求都使用自定义的授权管理器
http.authorizeHttpRequests(authorize -> authorize
// 对所有请求都需要开启授权
.anyRequest()
// 对于其他的请求,使用指定的授权管理器进行处理
.access(new AuthorizationManager<RequestAuthorizationContext>() {
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {
// 授权逻辑
return new AuthorizationDecision(true);
}
})
-
忽略静态资源
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/css/**").permitAll())
1.2.8 方法注解授权
- 方法授权注解的是如何工作的
@Service
public class MyCustomerService {
@PreAuthorize("hasAuthority('permission:read')")
@PostAuthorize("returnObject.owner == authentication.name")
public Customer readCustomer(String id) { ... }
}
- Spring AOP调用readCustomer的代理方法。在代理的切面中,它调用与@PreAuthorize切入点匹配的AuthorizationManagerBeforeMethodInterceptor
- 拦截器调用PreAuthorizeAuthorizationManager的check方法进行权限校验
- 授权管理器使用MethodSecurityExpressionHandler来解析注解的SpEL表达式,并从包含Supplier和MethodInvocation的MethodSecurityExpressionRoot中构造相应的EvaluationContext,拦截器使用这个上下文对表达式求值;具体来说,它从Authentication读取身份验证,并检查它是否在其权限集合中具有读取权限
- 如果校验通过,那么Spring AOP将继续调用该方法。
- 如果没有,拦截器发布一个AuthorizationDeniedEvent事件并抛出一个AccessDeniedException,由ExceptionTranslationFilter捕获并向响应返回一个403状态码
- 方法返回后,Spring AOP调用一个与@PostAuthorize切入点匹配的AuthorizationManagerAfterMethodInterceptor,操作与PreAuthorizeAuthorizationManager相同,但是使用的是PostAuthorizeAuthorizationManager
- 如果校验通过,处理将继续正常进行
- 如果校验失败,则拦截器发布AuthorizationDeniedEvent并抛出AccessDeniedException,由ExceptionTranslationFilter捕获并向响应返回一个403状态码
-
授权注解的切入点和方法拦截器
使用
@EnableMethodSecurity
开启下面注解的支持
每一个注解都有自身的切入点,并且也有自己的方法拦截器切入点细节可以看AuthorizationMethodPointcuts
@PreAuthorize
使用拦截器: AuthorizationManagerBeforeMethodInterceptor.preAuthorize()
依赖的授权管理器: PreAuthorizeAuthorizationManager
@PostAuthorize
使用拦截器: AuthorizationManagerAfterMethodInterceptor.postAuthorize()
依赖的授权管理器: PostAuthorizeAuthorizationManager
@PreFilter
使用拦截器: PreFilterAuthorizationMethodInterceptor
@PostFilter
使用拦截: PostFilterAuthorizationMethodInterceptor
@Secured
使用拦截器: AuthorizationManagerBeforeMethodInterceptor.secured()
依赖的授权管理器: SecuredAuthorizationManager
JSR-250注解
RolesAllowed.class, DenyAll.class, PermitAll.class;
使用拦截器AuthorizationManagerBeforeMethodInterceptor.jsr250()
授权管理器为Jsr250AuthorizationManager
-
授权注解对SPEL表达式的支持
- 详情可以看MethodSecurityExpressionOperations和SecurityExpressionOperations
// 如果有这种情况的表达式,提供了简写的方案 @PreAuthorize("hasAuthority('permission:read') || hasRole('ADMIN')") // 配置一个具有层次结构的Bean @Bean static RoleHierarchy roleHierarchy() { // 使用 ">" 自动将permission:read权限赋予给ROLE_ADMIN角色 return new RoleHierarchyImpl("ROLE_ADMIN > permission:read"); } // 简写之后 @PreAuthorize("hasAuthority('permission:read')")
-
请求级别的授权与方法级别的授权比较,该如何选择
- 当我们使用方法级别的注解进行权限校验,可能导致没有标注权限注解的方法不进行授权,为了处理这种情况,可以在HttpSecurity实例中配置一个请求级别的授权,接受所有请求的授权
请求级别 | 方法级别 | |
---|---|---|
授权类型 | 粗粒度 | 细粒度 |
配置位置 | 在配置类中声明 | 局部的方法声明注解 |
配置风格 | DSL | 注解 |
授权定义 | 编程定义 | SPEL |
-
权限注解的使用案例
- @PreAuthorize和@PostAuthorize
@Component public class BankService { @PreAuthorize("hasRole('ADMIN')") public Account readAccount(Long id) { } @PostAuthorize("returnObject.owner == authentication.name") public Account readAccount(Long id) { } // 当我们觉得每一个方法都需要写相同的表达式的时候,我们可以使用自定义的注解,包含这个通用表达式的注解 @RequireOwnership public Account readAccount(Long id) { } // 当我们觉得每一个方法都需要写相同的表达式的时候,我们可以使用自定义的注解,包含这个通用表达式的注解 @IsAdmin public Account readAccounts(Long id) { } } @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PostAuthorize("returnObject.owner == authentication.name") public @interface RequireOwnership {} @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @PreAuthorize("hasRole('ADMIN')") public @interface IsAdmin {}
-
@PostAuthorize(参数过滤)与@PostAuthorize(返回值过滤)
@Component public class BankService { // filterObject: 过滤的参数对象,参数为Account... accounts // 条件: 过滤Account对象中owner属性值为authentication.name名称的数据 @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Account... accounts) { return updated; } // filterObject: 过滤的返回值对象,返回值Collection<Account> // 条件: 过滤Account对象中owner属性值为authentication.name名称的数据 @PostFilter("filterObject.owner == authentication.name") public Collection<Account> readAccounts(String... ids) { return accounts; } } // 参数过滤支持数据,集合,流,Map @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Account[] accounts) @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Collection<Account> accounts) @PreFilter("filterObject.value.owner == authentication.name") public Collection<Account> updateAccounts(Map<String, Account> accounts) @PreFilter("filterObject.owner == authentication.name") public Collection<Account> updateAccounts(Stream<Account> accounts) // 返回值过滤,支持数组,集合和Map和流 @PostFilter("filterObject.owner == authentication.name") public Account[] readAccounts(String... ids) @PostFilter("filterObject.value.owner == authentication.name") public Map<String, Account> readAccounts(String... ids) @PostFilter("filterObject.owner == authentication.name") public Stream<Account> readAccounts(String... ids)
-
@Secured和JSR-250注解
- @Secured它是遗留的选项,可以使用@PreAuthorize代替
// 需要开启对@Secured的支持 @EnableMethodSecurity(securedEnabled = true) // 开启jsr250支持 @RolesAllowed, @PermitAll, @DenyAll. @EnableMethodSecurity(js r250Enabled = true)
-
使用自定义Bean的SPEL进行参数校验,和SPEL的使用整理
// 定义一个Bean @Component("authz") public class AuthorizationLogic { public boolean decide(MethodSecurityExpressionOperations operations) { } } @Controller public class MyController { // 使用自定义的Bean进行权限校验 @PreAuthorize("@authz.decide(#root)") @GetMapping("/endpoint") public String endpoint() { } } // 如果想让SPEL表达式可以使用到参数,可以使用@P注解来标识 // 如果是SpringData,使用@Param也可以获取 // 使用的是DefaultSecurityParameterNameDiscoverer进行参数解析的 @PreAuthorize("hasPermission(#c, 'write')") public void updateContact(@P("c") Contact contact);
-
如果只想启用部分权限注解,我们可以覆盖默认的配置
@Configuration
// 关闭pre,post的权限注解支持,那么就不会导入PrePostMethodSecurityConfiguration相关切面拦截器配置
@EnableMethodSecurity(prePostEnabled = false)
class MethodSecurityConfig {
// 根据自己的需求来提供某个注解的切面拦截器的支持
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize() {
return AuthorizationManagerBeforeMethodInterceptor.postAuthorize();
}
}
-
使用自定义的授权管理器完成权限注解的校验
// 其中,MethodInvocation: 表示参数,MethodInvocationResult表示结果 @Component public class MyAuthorizationManager implements AuthorizationManager<MethodInvocation>, AuthorizationManager<MethodInvocationResult> { @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) { } @Override public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocationResult invocation) { } } // 配置自己的注解注解校验规则 @Configuration @EnableMethodSecurity(prePostEnabled = false) class MethodSecurityConfig { @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor preAuthorize(MyAuthorizationManager manager) { return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(manager); } @Bean @Role(BeanDefinition.ROLE_INFRASTRUCTURE) Advisor postAuthorize(MyAuthorizationManager manager) { return AuthorizationManagerAfterMethodInterceptor.postAuthorize(manager); } }
-
使用自定义方法注解表达式处理器来解析注解中的SPEL表达式
// 可以继承DefaultMethodSecurityExpressionHandler进行扩展
@Bean
static MethodSecurityExpressionHandler methodSecurityExpressionHandler(RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setRoleHierarchy(roleHierarchy);
return handler;
}
@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication, MethodInvocation mi) {
StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
MethodSecurityExpressionOperations delegate = (MethodSecurityExpressionOperations) context.getRootObject().getValue();
MySecurityExpressionRoot root = new MySecurityExpressionRoot(delegate);
context.setRootObject(root);
return context;
}
}
- 使用自定义的Aspectj切点表达式来校验权限
- 这样,即使没有添加注解,符合该切面该表达式的切入点都会校验AMIN权限
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static Advisor protectServicePointcut() {
AspectJExpressionPointcut pattern = new AspectJExpressionPointcut();
pattern.setExpression("execution(* luck.spring.security.controller.*TestApi.*(..))");
return new AuthorizationManagerBeforeMethodInterceptor(pattern, hasRole("ADMIN"));
}
- 在某些时刻,切面拦截器的顺序是很重要的,例如权限注解中同时包含事务注解,当权限注解校验失败的时候,我们希望事务回滚,这个时候,我们就需要事务的拦截器需要在权限检验注解@PostAuthorize之前执行来开启事务,以便在权限校验失败的时候进行事务回滚
@PreFilter order is 100, @PreAuthorize's is 200 以此推类
在@EnableTransactionManagement中,默认的order是Int的最大值,优先级最低,所以如果要在权限注解拦截器执行之前开启事务,那么就需要让事务拦截器在权限注解拦截器之前
可以将事务注解的顺序修改为0,大于权限注解的顺序就行,具体的权限还是要根据实际情况
@EnableTransactionManagement(order = 0)
1.3 SpringSecurity所有的过滤器
// 所有的过滤器
final class FilterOrderRegistration {
FilterOrderRegistration() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(DisableEncodeUrlFilter.class, order.next());
put(ForceEagerSessionCreationFilter.class, order.next());
put(ChannelProcessingFilter.class, order.next());
order.next(); // gh-8105
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextHolderFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
put(CsrfFilter.class, order.next());
put(LogoutFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());
this.filterToOrder.put("org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter", order.next());
put(X509AuthenticationFilter.class, order.next());
put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());
this.filterToOrder.put("org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter", order.next());
put(UsernamePasswordAuthenticationFilter.class, order.next());
order.next();
put(DefaultLoginPageGeneratingFilter.class, order.next());
put(DefaultLogoutPageGeneratingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(DigestAuthenticationFilter, "org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter", order.next());
put(BasicAuthenticationFilter.class, order.next());
put(RequestCacheAwareFilter.class, order.next());
put(SecurityContextHolderAwareRequestFilter.class, order.next());
put(JaasApiIntegrationFilter.class, order.next());
put(RememberMeAuthenticationFilter.class, order.next());
put(AnonymousAuthenticationFilter.class, order.next());
this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());
put(SessionManagementFilter.class, order.next());
put(ExceptionTranslationFilter.class, order.next());
put(FilterSecurityInterceptor.class, order.next());
put(AuthorizationFilter.class, order.next());
put(SwitchUserFilter.class, order.next());
}
}