1.概述
Spring Security是一个基于Spring框架的安全性框架,它提供了一系列的API和扩展点,可以帮助开发人员在应用程序中轻松地实现安全认证和授权控制。
我们可以理解为Spring Security维护了一组我们可以自定义的访问规则,每次访问都会去进行规则比对,满足规则的才放行。
这些规则可以有很多维度,本文会以最基础的基于角色的控制入手逐步扩展详细介绍Spring Security。
基于角色的控制,我们可以理解为Spring Security为我们维护了一张“白名单”,而登录就是去白名单里进行比对。
2.登录
2.1.默认用户
依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
依赖引入后其实spring security就已经生效了,此时访问我们的接口会自动转跳spring security内置的登录页,验证通过后才会转跳到后端接口:
默认用户名:user
默认密码:会在日志中输出
2.2.自定义用户
spring security给我们提供了接口来配置security,其中就包括自定义用户和角色:
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(). //所有用户都可以访问/all antMatchers("/all").permitAll(). //admin可以访问/admin antMatchers("/admin").hasRole("admin"); //如果验证未通过,转跳spring security自带的登录页面进行登录 //如果不配置此处的步骤,验证未通过则会直接返回403访问被拒绝。 http.formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义两个用户user、admin,user对应user角色,admin对应admin角色 auth.inMemoryAuthentication() .withUser("user").password("123").roles("user") .and() .withUser("admin").password("456").roles("user", "admin"); } }
2.3.加密
完成上面的配置这时候我们再访问我们的接口,转跳登录页后输入我们定义的有对应访问用户名密码,比如admin 456,似乎就应该能正常访问到我们的接口了。但是实际上会报错:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
这是因为我们在定义用户的时候密码没有加密,自spring security 5.0开始就要求必须对密码进行加密,否则,在进行密码比较时,就会出现无法解析密码加密算法的异常。我们当前用的主流版本一定是5.0以上,所以加密是必须的。
spring security提供了一个PasswordEncoder接口给我们自定器,在加密器里可以通过方法重写来自定义加密过程。但是实际使用上不用费力去自己写一个加密算法,security给我们准备了多种加密器、多种加密方法,而且自己写的也肯定没有开源的稳定和好用。
spring security提供的加密器如下:
BCryptPasswordEncoder:这是最常用的加密算法之一,它使用哈希和随机盐来加密密码。
Pbkdf2PasswordEncoder:这也是一种密码加密算法,它使用基于密码的密钥导出函数(PBKDF2)来加密密码。
SCryptPasswordEncoder:这是一种基于内存的密码哈希算法,它使用大量的内存来防止散列碰撞攻击。
NoOpPasswordEncoder:这是一种不安全的加密算法,它仅仅是将明文密码作为加密后的密码。不建议在生产环境中使用。
我们就挑选BCryptPasswordEncoder来对密码进行加密,修改一下,在定义用户名密码的时候BCryptPasswordEncoder来进行加密:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义两个用户user、admin,user对应user角色,admin对应admin角色 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("user").password("123").roles("user") .and() .withUser("admin").password("456").roles("user", "admin"); }
加密之后我们再在登陆页面进行登录的时候就不能用明文的admin 456来进行登录了,需要将456使用同样的加密算法加密后的密文密码来登录。
2.4.绕过加密
在实际生产环境种使用密文密码当然是无可厚非的,但是在开发、测试阶段会很麻烦,有没有绕过的办法喃?有的,{noop}","no operation"的缩写,表示不执行任何操作。如果密码的前缀是"{noop}",则Spring Security会将其识别为明文密码,不进行加密,直接存储到数据库或内存中。可以将上面的代码改成:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //自定义两个用户user、admin,user对应user角色,admin对应admin角色 auth.inMemoryAuthentication() .withUser("user").password("{noop}123").roles("user") .and() .withUser("admin").password("{noop}456").roles("user", "admin"); }
2.5.怎么传递用户信息
由于我们的请求我们都是在登陆界面输入用户名密码,实际使用中不可能每次都这样去做,怎么把用户名、密码携带在请求里传给security喃?这却决于配置为哪种,支持两种方式:
- 表单
- 放在请求头中
表单:
后端配置:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() //指定用表单的方式登录 .formLogin() //登录地址,不配置的话会有默认值,默认是login,可以用这个配置来设置新的请求页 .loginPage("/login") //配置用户名的参数名,不配置的话会有默认值,默认是username .usernameParameter("username") //配置密码的参数名,不配置的话会有默认值,默认是password .passwordParameter("password") .permitAll() }
前端表单:
<form action="/login" method="post"> <label for="username">Username:</label> <input type="text" id="username" name="username"><br><br> <label for="password">Password:</label> <input type="password" id="password" name="password"><br><br> <input type="submit" value="Submit"> </form>
当用户提交表单后,会向后端发送一个POST请求,请求的URL为/login,请求参数包含用户名和密码。
放在表头中:
在请求头中添加Authorization字段,将用户名和密码进行Base64编码后传递给后端进行验证。后端配置示例:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .httpBasic() .permitAll(); }
2.6.记住我
security支持“记住我”这个功能,是基于token来实现得,开启记住我后,登陆成功后会返给客户端一个cookie,用这个cookie来实现记住我的效果,配置示例如下:
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeHttpRequests(). antMatchers("/all").permitAll(). antMatchers("/admin").hasRole("admin").and() .formLogin().and() //开启记住我功能 .rememberMe() //设置返回的cookie的键名 .rememberMeParameter("remember-me") //设置过期时间 .tokenValiditySeconds(7*24*60*60); }