SpringSecurity 基础
什么是安全框架
安全框架顾名思义,就是解决系统安全问题的框架。任何应用开发的计划阶段都应该确定一组特定的安全需求,如身份验证、授权和加密方式。不使用安全框架之前,我们需要手动处理每个资源的访问控制,针对不同的项目都需要做不同对处理,此时就会显得非常麻烦,并且低效率引起的额外开销会延缓开发周期。使用安全框架,使开发团队能够选择最适合这些需求的框架,可以通过配置的方式实现对资源的访问限制,使得开发更加的高效。
常用的安全框架
Spring Security: Spring 家族一员,是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(控制反转)、DI(依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理功能。使用 Shiro 的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
SpringSecurity 简介
Spring Security 是一个高度自定义的安全框架,利用 Spring loC、DI 和 AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大星重复代码的工作。
使用 Spring Secruity 的原因有很多,但大部分都是发现了 javaEE 的 Servlet 规范或 EJ8 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR 或 EAR 级别无法移植,因此如果你更换服务器环境,还有大星工作去重新配置你的应用程序,使用 Spring Security 解决了这些问题,并且提供了可定制的安全功能,比如认证和授权。
SpringBoot 没有发布之前,Shiro 应用更加广泛,因为 Shiro 是一个强大且易用的 Java 安全框架,能够非常清晰的处理身份验证、授权、管理会话以及密码加密。利用其易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。但是 Shiro 只是一个框架而已,其中的内容需要自己的去构建,前后是自己的,中间是Shiro帮我们去搭建和配置好的。
SpringBoot 发布后,随着其快速发展,Spring Security 重新进入人们的视野。SpringBoot 解决了 Spring Security 各种复杂的配置,Spring Security 在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全,也就是说 Spring Security 除了不能脱离 Spring,Shiro 的功能它都有。
-
在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
-
在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
SpringSecurity 入门
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
导入配置
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 禁用csrf(跨站请求伪造)
.cors().and().csrf().disable()
// 设置表单登陆以及登录页面,自动开启登录页,如果没有登录,没有权限就会来到登录页面
.formLogin().loginPage("/login.html").and()
// 过滤请求
.authorizeRequests()
// 访问此地址不需要进行身份认证,允许直接访问,防止重定向死循环
.antMatchers("/login.html").permitAll()
// 除上面外的所有请求全部需要鉴权认证,访问任何资源都需要身份认证
.anyRequest().authenticated();
}
}
登录测试
启动服务之后,如果只实现一个 WebSecurityConfigurerAdapter 然后重写一下 configure 方法,效果会默认使用Spring Security 的登录页 ,同时项目启动时后台会打印出一个默认的密码,然后使用任意账号就可以进行登录访问指定的资源。
SpringSecurity 核心
基本原理
Spring Security 所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源,并且采用的是责任链的设计模式。
Spring Security 对 Web 资源的保护是靠过滤器链(Filter Chain)实现的。当初始化 Spring Security 时,会创建一个名为 springSecurityFilterChain 的 Servlet 过滤器链,类型 FilterChainProxy,它实现 Filter,因此外部的请求会经过此类,下图是 Spring Security 过虑器链结构图:
FilterChainProxy 是一个代理,真正起作用的是 FilterChainProxy 中 SecurityFilterChain 所包含的各个 Filter,同时这些 Filter 作为 Bean 被 Spring 管理,它们是 Spring Security 核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理。
Spring Security 功能的实现主要是由一系列过滤器链相互配合完成,如下图:
下面介绍过滤器链中主要的几个过滤器及其作用:
-
WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
-
SecurityContextPersistenceFilter :每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除。
-
UsernamePasswordAuthenticationFilter :用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。
-
HeaderWriterFilter:用于将头信息加入响应中。
-
CsrfFilter:用于处理跨站请求伪造。
-
LogoutFilter:用于处理退出登录。
-
DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
-
BasicAuthenticationFilter:检测和处理 http basic 认证。
-
RequestCacheAwareFilter:用来处理请求的缓存。
-
SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
-
AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
-
SessionManagementFilter:管理 session 的过滤器
-
ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
-
FilterSecurityInterceptor: 是用于保护web资源的,使用 AccessDecisionManager 对当前用户进行授权访问。
-
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
下面看一下 Spring Security 整个执行流程图,只要把 Spring Security 的执行过程弄明白了,这个框架就会变得很简单:
流程说明:
-
客户端发起一个请求,进入 Security 过滤器链。
-
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
-
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
-
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
核心配置
系统集成 Spring Security 后,通过创建配置类并继承 WebSecurityConfigurerAdapter 类,这个类里面可以完成上述流程图的所有配置,也就是认证及授权。接下来我们通过一个完整的配置类来进行详细认识:
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 资源请求配置
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// ...
}
/**
* 全局安全性配置
*/
public void configure(WebSecurity web) {
web.ignoring().antMatchers(new String[]{"/v3/api-docs", "/swagger-resources/configuration/ui",
"/swagger-resources", "/swagger-ui.html", "/swagger-ui/*", "/modeler/**", "/**/doc.html",
"/favicon.ico", "/definition/**", "/activiti/**", "/**/*.css", "/**/*.js", "/**/*.png",
"/**/*.gif", "/swagger-resources/**", "/**/*.ttf", "/upload/**", "/process/read-resource/**",
"/ueditor/**", "/**/export/**", "/**importGdCache/**", "/**/sysGlobalConfig/**", "/OAuth/**", "/v1/**"});
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
configure(AuthenticationManagerBuilder auth):身份认证接口
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让 Spring Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);
如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。
如果重写了该方法,Spring Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
configure(WebSecurity web):全局安全性配置
此方法用于配置影响全局安全性的配置,比如配置资源,设置调试模式,通过实现自定义防火墙定义拒绝请求,一般用于配置全局的某些通用事物,比如静态资源等。
configure(HttpSecurity http):资源请求配置
这个方法是整个 Spring Security 的核心,也是最复杂的部分,通过案例简单的说明一些常用配置,详细说明会在权限控制中说明。
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().loginPage("/login_page") // 自定义登录页
.passwordParameter("username") // 用户名属性名
.passwordParameter("password") // 密码属性名
.loginProcessingUrl("/sign_in") // 登录请求路径
.permitAll(); // 代表任意用户可访问
.cors().and()).csrf().disable()) // 禁用csrf(跨站请求伪造)
.authorizeRequests() // 过滤请求
.antMatchers(new String[]{"/upload/**", "/definition/**", "/activiti/**"}).permitAll() // 放行匹配请求
.anyRequest().authenticated().and() //除上面外的所有请求全部需要鉴权认证
.exceptionHandling().accessDeniedHandler((AccessDeniedHandler) this.accessDeniedHandler); // 异常解析器
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 退出登录
}
}
权限控制
匹配规则
-
anyRequest():表示匹配所有的请求,一般情况都会使用此方法,设置全部内容都需要进行认证,比如
anyRequest().authenticated();
。 -
antMatcher(String... antPatterns): 表示匹配指定的请求,参数是不定向参数,每个参数是一个 ant 表达式,? 表示匹配一个字符,* 表示匹配 0~N 个字符,** 表示匹配 0~N 个目录。例如
antMatchers( "/**/*.js").permitAll()
。 -
regexMatchers(String... regexPatterns):使用正则表达式进行匹配,与 antMatchers() 主要的区别就是参数,antMatchers()参数是 ant 表达式,二 regexMatchers() 参数是正则表达式。例如
.regexMatchers( ".+[.]js").permitAll()
。 -
mvcMatchers():适用于配置了 servletPath 的情况。.servletPath() 是 mvcMatchers() 返回值特有的方法,例如
.mvcMatchers( "demo").servletPath( "/bjsxt").permitAll()
等价于antMatchers( "/bjsxt/demo").permitAll()
。
访问控制
-
permitAll():表示所匹配的 URL 任何人都允许访问,也就是不需要认证,随意访问。
-
anonymous():表示可以匿名访问匹配的 URL,只是设置为 anonymous() 的 URL 会执行 filter 链中,比如说浏览商城时。
-
authenticated():表示所匹配的 URL 都需要被认证才能访问,也就是用户登录后可访问。
-
denyAll():表示所匹配的 URL 都不允许被访问。
-
rememberMe():只有被 remember me 的用户才能访问。
-
fullyAuthenticated():如果用户不是被 remember me 的,才可以访问。
角色控制
-
hasRole():用户具备某个角色即可访问资源,此方法会自动给传入的字符串加上 ROLE_ 前缀,例如 hasRole("list"),表示拥有 ROLE_list 权限即可访问。
-
hasAnyRole():用户具备多个角色中的任意一个即可访问资源,例如 hasAnyRole("admin", "save"),只要具备其中一个角色,即可访问资源。
-
hasAuthority():类似于 hasRole,但是不会添加 ROLE_ 前缀,也就是说,使用 hasAuthority 更具有一致性,你不用考虑要不要加 ROLE_ 前缀,数据库什么样这里就是什么样!
-
hasAnyAuthority():类似于 hasAnyRole,只是没有前缀。
注解控制
当我们使用注解之前,必须通过 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) 开启注解。
-
@PreAuthorize:方法执行前进行权限检查,允许使用 SpEL(pring表达式语言)。
-
@PostAuthorize:方法执行后进行权限检查,基本不用。
-
@Secured:类似于 @PreAuthorize,但是不允许使用 SpEL(pring表达式语言)。
例如:
@Controller
public class HelloController {
// 只有当前登录用户名为 javaboy 的用户才可以访问该方法。
@PreAuthorize("principal.username.equals(‘javaboy‘)")
public String hello() {
return "hello";
}
// 表示访问该方法的用户必须具备 admin 角色
@PreAuthorize("hasRole(‘admin‘)")
public String admin() {
return "admin";
}
// 表示访问该方法的 age 参数必须大于 98,否则请求不予通过。
@PreAuthorize("#age>98")
public String getAge(Integer age) {
return String.valueOf(age);
}
// 表示该方法的用户必须具备 user 角色,但是注意 user 角色需要加上 ROLE_ 前缀。
@Secured({"ROLE_user"})
public String user() {
return "user";
}
}
请求认证
让我们仔细分析认证过程:
-
当用户发送登录请求的时候,首先进入到 UsernamePasswordAuthenticationFilter 中进行校验。
-
UsernamePasswordAuthenticationFilter 通过 attemptAuthentication 方法会获取用户的username以及password参数的信息,封装为 UsernamePasswordAuthenticationToken 对象,最后会进入 AuthenticationManager 接口的实现类 ProviderManager 中。AuthenticationManager(认证管理器) 本身不包含验证的逻辑,它的作用是用来管理 AuthenticationProvider。
-
进入 ProviderManager 类调用 authenticate() 方法,通过循环遍历判断它是否支持这种登录方式,具体的登录方式有表单登录,qq登录,微信登录等。如果支持则会进入A uthenticationProvider 接口的抽象实现类 AbstractUserDetailsAuthenticationProvider 中调用 authenticate() 方法对用户的身份进入校验。
-
进入 AbstractUserDetailsAuthenticationProvider 的 authenticate方法之后,UserDetail 的 user 对象是否为空,如果为空,表示还没有认证,就需要调用 DaoAuthenticationProvider 类的 retrieveUser 方法去获取用户的信息。
-
该扩展类的 retrieveUser 方法中调用 UserDetailsService 这个接口的实现类的 loadUserByUsername 方法去获取用户信息。如果需要自定义实现,则可以实现 UserDetails 接口,编写自己的逻辑,从数据库中获取用户密码等权限信息返回。
-
获取到用户信息之后,返回到 AbstractUserDetailsAuthenticationProvider 类中调用 createSuccessAuthentication() 方法,通过 PasswordEncoder 对比用户信息是否与 AuthenticationManager 一直,如果一直,则认证通过(setAuthenticated(true))。
-
认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息, 身份信息,细节信息,但密码通常会被移除) Authentication 实例。
请求授权
让我们仔细分析授权过程:
-
已认证用户访问受保护的 web 资源将被 SecurityFilterChain 中的 FilterSecurityInterceptor 的子类拦截。
-
FilterSecurityInterceptor 会从 SecurityMetadataSource 的 getAttributes() 方法,获取要访问当前资源所需要的权限。其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则。
-
FilterSecurityInterceptor 会调用 AccessDecisionManager(授权决策器)的 decide() 方法进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。