Spring Security入门学习
theme: cyanosis
Spring Security系列文章
- 认证与授权之Cookie、Session、Token、JWT
- 基于Session的认证与授权实践
- Spring Security入门学习
认识Spring Security
Spring Security 是为基于 Spring 的应用程序提供声明式安全保护的安全性框架。Spring Security 提供了完整的安全性解决方案,它能够在 Web 请求级别和方法调用级别处理身份认证和授权。因为基于 Spring 框架,所以 Spring Security 充分利用了依赖注入(dependency injection, DI)和面向切面的技术。
核心功能
对于一个权限管理框架而言,无论是 Shiro 还是 Spring Security,最最核心的功能,无非就是两方面:
- 认证
- 授权
通俗点说,认证就是我们常说的登录,授权就是权限鉴别,看看请求是否具备相应的权限。
认证(Authentication)
Spring Security 支持多种不同的认证方式,这些认证方式有的是 Spring Security 自己提供的认证功能,有的是第三方标准组织制订的,主要有如下一些:
一些比较常见的认证方式:
- HTTP BASIC authentication headers:基于IETF RFC 标准。
- HTTP Digest authentication headers:基于IETF RFC 标准。
- HTTP X.509 client certificate exchange:基于IETF RFC 标准。
- LDAP:跨平台身份验证。
- Form-based authentication:基于表单的身份验证。
- Run-as authentication:用户用户临时以某一个身份登录。
- OpenID authentication:去中心化认证。
除了这些常见的认证方式之外,一些比较冷门的认证方式,Spring Security 也提供了支持。
- Jasig Central Authentication Service:单点登录。
- Automatic "remember-me" authentication:记住我登录(允许一些非敏感操作)。
- Anonymous authentication:匿名登录。
- ......
作为一个开放的平台,Spring Security 提供的认证机制不仅仅是上面这些。如果上面这些认证机制依然无法满足你的需求,我们也可以自己定制认证逻辑。当我们需要和一些“老破旧”的系统进行集成时,自定义认证逻辑就显得非常重要了。
授权(Authorization)
无论采用了上面哪种认证方式,都不影响在 Spring Security 中使用授权功能。Spring Security 支持基于 URL 的请求授权、支持方法访问授权、支持 SpEL 访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持 RBAC 权限模型等,总之,我们常见的权限管理需求,Spring Security 基本上都是支持的。
项目实践
创建 maven 工程
项目依赖如下:
```xml
提供一个简单的测试接口,如下:
```java @RestController public class HelloController {
@GetMapping("/hello") public String hello() { return "hello,hresh"; }
@GetMapping("/hresh") public String sayHello() { return "hello,world"; } } ```
再创建一个启动类,如下:
```java @SpringBootApplication public class SecurityInMemoryApplication {
public static void main(String[] args) { SpringApplication.run(SecurityInMemoryApplication.class, args); } } ```
在 Spring Security 中,默认情况下,只要添加了依赖,我们项目的所有接口就已经被统统保护起来了,现在启动项目,访问 /hello
接口,就需要登录之后才可以访问,登录的用户名是 user,密码则是随机生成的,在项目的启动日志中,如下所示:
java
Using generated security password: 21596f81-e185-4b6a-a8ff-1b21e2a60c6f
我们尝试访问 /hello 接口,因为该接口被 Spring Security 保护起来了,重定向到 /login 接口,如下图所示:
输入账号和密码后,即可访问 /hello 接口。
那么如何自定义登录用户信息呢?以及 Spring Security 如何知道我们想要支持基于表单的身份验证?
认证
启用web安全性功能
Spring Security 提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。
在 Spring Security 5.7版本之前,或者 SpringBoot2.7 之前,我们都是继承 WebSecurityConfigurerAdapter 来配置。
```java @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
//定义用户信息服务(查询用户信息) @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); return manager; }
//密码编码器,不加密,字符串直接比较 @Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
@Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/hello"); }
//安全拦截机制(最重要) @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .and() .httpBasic(); } } ```
Spring Security 提供了这种链式的方法调用。上面配置指定了认证方式为 HTTP Basic 登录,并且所有请求都需要进行认证。
这里有一点需要注意,我没并没有在 Spring Security 配置类上使用@EnableWebSecurity 注解。这是因为在非 Spring Boot 的 Spring Web MVC 应用中,注解@EnableWebSecurity 需要开发人员自己引入以启用 Web 安全。而在基于 Spring Boot 的 Spring Web MVC 应用中,开发人员没有必要再次引用该注解,Spring Boot 的自动配置机制 WebSecurityEnablerConfiguration 已经引入了该注解,如下所示:
```java package org.springframework.boot.autoconfigure.security.servlet;
// 省略 imports 行
@Configuration( proxyBeanMethods = false ) @ConditionalOnMissingBean( name = {"springSecurityFilterChain"} ) @ConditionalOnClass({EnableWebSecurity.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) @EnableWebSecurity class WebSecurityEnablerConfiguration { WebSecurityEnablerConfiguration() { } } ```
实际上,一个 Spring Web 应用中,WebSecurityConfigurerAdapter 可能有多个 , @EnableWebSecurity 可以不用在任何一个WebSecurityConfigurerAdapter 上,可以用在每个 WebSecurityConfigurerAdapter 上,也可以只用在某一个WebSecurityConfigurerAdapter 上。多处使用@EnableWebSecurity 注解并不会导致问题,其最终运行时效果跟使用@EnableWebSecurity 一次效果是一样的。
在 userDetailsService()方法中,我们返回了一个 UserDetailsService 给 Spring 容器,Spring Security 会使用它来获取用户信息。我们暂时使用 InMemoryUserDetailsManager 实现类,并在其中分别创建了zhangsan、lisi两个用户,并设置密码和权限。
在configure(HttpSecurity http)
方法中进入如下配置:
- 确保对我们的应用程序的任何请求都要求用户进行身份验证
- 允许用户使用基于表单的登录进行身份验证
- 允许用户使用HTTP基本身份验证进行身份验证
注意上述还有一个 passwordEncoder()方法,在 IDEA 中会提示 NoOpPasswordEncoder 已过期。这是因为 Spring Security 5对 PasswordEncoder 做了相关的重构,原先默认配置的 PlainTextPasswordEncoder(明文密码)被移除了,想要做到明文存储密码,只能使用一个过期的类来过渡。
java
//加入
//已过期
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
Spring Security 提供了多种类来进行密码编码,并作为了相关配置的默认配置,只不过没有暴露为全局的 Bean。在实际应用中使用明文校验密码肯定是存在风险的,NoOpPasswordEncoder 只能存在于 demo 中。
```java //实际应用 @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); }
//加密方式与对应的类 bcrypt - BCryptPasswordEncoder (Also used for encoding) ldap - LdapShaPasswordEncoder MD4 - Md4PasswordEncoder MD5 - new MessageDigestPasswordEncoder("MD5") noop - NoOpPasswordEncoder pbkdf2 - Pbkdf2PasswordEncoder scrypt - SCryptPasswordEncoder SHA-1 - new MessageDigestPasswordEncoder("SHA-1") SHA-256 - new MessageDigestPasswordEncoder("SHA-256") sha256 - StandardPasswordEncoder ```
但是在 Spring Security 5.7版本之后(包括5.7版本),或者 SpringBoot2.7 之后,WebSecurityConfigurerAdapter 就过期了,虽然可以继续使用,但看着比较别扭。
看 5.7版本官方文档是如何解释的:
以前我们自定义类继承自 WebSecurityConfigurerAdapter 来配置我们的 Spring Security,我们主要是配置两个东西:
- configure(HttpSecurity)
- configure(WebSecurity)
前者主要是配置 Spring Security 中的过滤器链,后者则主要是配置一些路径放行规则。
现在在 WebSecurityConfigurerAdapter 的注释中,人家已经把意思说的很明白了:
- 以后如果想要配置过滤器链,可以通过自定义 SecurityFilterChain Bean 来实现。
- 以后如果想要配置 WebSecurity,可以通过 WebSecurityCustomizer Bean 来实现。
我们对上文中的 SecurityConfig 文件做一下改动,试试新版中该如何配置。
```java @Configuration public class SecurityConfig {
@Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build()); manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build()); return manager; }
@Bean public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
@Bean WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().antMatchers("/hello"); }
@Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .permitAll() .and() .csrf().disable(); return http.build(); } } ```
此时重启项目,你会发现 /hello
也是可以直接访问的,就是因为这个路径不经过任何过滤器。
个人觉得新写法更加直观,可以清楚的看到 SecurityFilterChain 是关于过滤器链配置的,与我们理论知识提到的过滤器知识是一致的。
测试
访问 http://localhost:8086/hello,可以直接看到页面内容,不需要输入账号密码。
访问 http://localhost:8086/hresh,则需要账号密码,即我们配置的 zhangsan 和 lisi 用户。
在测试过程中,你可能会发现这样几个问题:
1、直接访问 http://localhost:8086,默认会跳转到 /login 页面,该配置位于 UsernamePasswordAuthenticationFilter 类文件中,如果你想自定义登录页面,可以这样修改:
java
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// .loginPage("/login.html")
.loginProcessingUrl("/login")
2、表单登录时,账号密码默认字段为 username 和 password。
3、按理来说,登录成功之后是跳到/页面,失败跳转到登录页,但因为我们这是 SpringBoot 项目,我们可以让它登录成功时返回json数据,而不是重定向到某个页面。默认情况下,账号密码输入错误会自动返回登录页面,所以此处我们就不处理失败的情况。
```java @Component public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write(objectMapper.writeValueAsString("登录成功")); } } ```
接着修改 securityFilterChain()方法
java
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.and()
.csrf().disable();
再次重启项目,在登录页面输入账号密码后,返回结果如下所示:
4、自定义登录页面,在 resource 目录下新建 static 目录,里面添加 login.html 文件,暂时未添加样式
```html
```
修改 securityFilterChain()方法
java
http.authorizeRequests() //表示开启权限配置
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated() //表示所有的请求都要经过认证之后才能访问
.and() // 链式编程写法
.formLogin() //开启表单登录配置
.loginPage("/login.html") // 配置登录页面地址
.loginProcessingUrl("/doLogin")
.permitAll()
.and()
.csrf().disable();
重启项目后, 再次访问 http://localhost:8086/,会重定向到 http://localhost:8086/login.html。
最后,总结一下 HttpSecurity 的配置,示例如下:
java
http.authorizeRequests() //表示开启权限配置
.anyRequest().authenticated() //表示所有的请求都要经过认证之后才能访问
.and() // 链式编程写法
.formLogin() //开启表单登录配置
.loginPage("/login.html") // 配置自定义登录页面地址
.loginProcessingUrl("/login") //配置登录接口地址
// .defaultSuccessUrl() //登录成功后的跳转页面
// .failureUrl() //登录失败后的跳转页面
// .usernameParameter("username") //登录用户名的参数名称
// .passwordParameter("password") // 登录密码的参数名称
// .successHandler(
// myAuthenticationSuccessHandler) //前后端分离的情况,并不想通过defaultSuccessUrl进行页面跳转,只需要返回一个json数据来告知前端
// .failureHandler(myAuthenticationFailureHandler) // 同理,替代failureUrl
// .permitAll()
.and()
.csrf().disable();// 禁用CSRF防御功能,测试可以先关闭
表单验证时,loginPage 与 loginProcessingUrl 区别:
- loginPage 配置自定义登录页面地址,loginProcessingUrl 默认与表单 action 地址一致;
- 如果只配置 loginPage 而不配置 loginProcessingUrl,那么 loginProcessingUrl 默认就是 loginPage;
- 如果只配置 loginProcessUrl,就会用不了自定义登陆页面,Security 会使用自带的默认登陆页面;
如果 loginProcessingUrl 默认与表单 action 地址不一致,那么它需要指向一个有效的地址,比如说 /doLogin.html,这要求我们在 static 目录下创建一个 doLogin.html 页面,此外,还需要在 controller 文件中增加如下方法:
java
@PostMapping("/doLogin")
public String doLogin() {
return "我登录成功了";
}
但是登录成功后并不会显示 doLogin.html 页面的内容,而是显示 /doLogin 的返回结果。同理,不配置 loginProcessingUrl,那么 loginProcessingUrl 默认就是 loginPage,即 loginProcessingUrl=login.html,与 doLogin.html 效果一样。
另外再介绍一下 Spring Security 中 defaultSuccessUrl 和 successForwardUrl 的区别:
假定在 defaultSuccessUrl 中指定登录成功的跳转页面为 /index
,那么存在两种情况:
- ① 浏览器中输入的是登录地址,登录成功后,则直接跳转到
/index
; - ② 如果浏览器中输入了其他地址,例如
http://localhost:8080/elseUrl
,若登录成功,就不会跳转到/index
,而是来到/elseUrl
页面。
defaultSuccessUrl 就是说,它会默认跳转到 Referer 来源页面,如果 Referer 为空,没有来源页,则跳转到默认设置的页面。
successForwardUrl 表示不管 Referer
从何而来,登录成功后一律跳转到指定的地址。
认证方式选择
在 WebSecurityConfigurerAdapter 类中有很多 configure()方法,除了上文提到的 HttpSecurity 和 WebSecurity 参数,还有一个 AuthenticationManagerBuilder 参数,源码如下:
```java protected void configure(AuthenticationManagerBuilder auth) throws Exception { this.disableLocalConfigureAuthenticationBldr = true; }
protected AuthenticationManager authenticationManager() throws Exception { if (!this.authenticationManagerInitialized) { this.configure(this.localConfigureAuthenticationBldr); if (this.disableLocalConfigureAuthenticationBldr) { this.authenticationManager = this.authenticationConfiguration.getAuthenticationManager(); } else { this.authenticationManager = (AuthenticationManager)this.localConfigureAuthenticationBldr.build(); }
this.authenticationManagerInitialized = true;
}
return this.authenticationManager; } ```
该类用于设置各种用户想用的认证方式,设置用户认证数据库查询服务 UserDetailsService
类以及添加自定义 AuthenticationProvider
类实例等
Spring Security 为配置用户存储提供了多个可选解决方案,包括:
- 基于内存的用户存储
- 基于 JDBC 的用户存储
- 以 LDAP 作为后端的用户存储
- 自定义用户详情服务
关于这四种方式就不详细介绍了,可以重点关注方案二和方案四,而在本项目中,我们直接在 SecurityConfig 中重写 userDetailsService 方法,并将 UserDetailsService 对象注入到 Spring 容器中。
授权
1、首先在 HelloController 中增加 r1 和 r2 资源。
```java @GetMapping(value = "/r/r1") public String r1() { return " 访问资源1"; }
@GetMapping(value = "/r/r2") public String r2() { return " 访问资源2"; } ```
2、修改 SecurityConfig 文件中的 securityFilterChain()方法
java
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
.anyRequest().authenticated()
.and()
.formLogin()
.successHandler(myAuthenticationSuccessHandler)
.permitAll()
.and()
.csrf().disable();
return http.build();
}
访问 r1、r2 资源,需要对应的权限,而且其他接口则只需要认证,并不需要授权。
3、测试
访问 http://localhost:8086 ,进入登录页面,输入正确的账号密码,提交后页面返回“登录成功”,如果是 zhangsan,则可以访问 r1资源,访问 r2则会报错,我们暂时未处理错误如下:
总结
关于 Spring Security 的学习先到这里,基本了解如何使用即可,我们继续后面的学习。
如果想要深入学习 Spring Security,推荐阅读《深入浅出Spring Security》,还包括配套的代码示例。
参考文献
- SpringBoot结合Liquibase实现数据库变更管理
- Spring Security结合JWT实现认证与授权
- Spring Security入门学习
- JVM系列之:你知道为什么要有两个 Survivor吗?关于卡表技术又有多少了解
- 高考报志愿:可惜当年填志愿时,没人告诉我这些...
- Java并发进阶之:Java内存模型(JMM)详解
- 工作那么久,该如何提升代码质量
- JVM系列之:你知道Java有多少种内存溢出吗
- JVM系列之:日志分析工具:GCViewer、VisualVM、GCeasy
- JVM系列之:GC调优基础以及初识jstat命令
- JVM系列之:关于即时编译器的其他一些优化手段
- Git实操小课堂
- 2020面试准备之并发进阶
- 2020面试准备之并发基础
- Java并发编程学习系列八:单例模式
- 全面学习volatile
- 基于SpringBoot将Json数据导入到数据库
- SpringCloud学习之Rest服务
- SpringCloud学习之Eureka
- 使用Kettle动态生成页码并实现分页数据同步