Spring Security框架
1.1. Spring Security简介
Spring Security是一个安全框架,在常规使用时,主要使用它的:
- 框架内自带对密码进行加密的工具包,可以对密码进行加密处理;
- 自带登录验证及获取登录用户的信息的机制(包括页面、提交数据后的处理),可以更加简单的实现登录、获取权限;
- 可以便利的实现授权访问(即访问某个路径需要某个权限等);
- 结合其它框架实现更多功能。
1.2. 添加依赖
在使用时,还是先在straw
父级项目的pom.xml
中添加依赖:
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
然后,在straw-api
项目中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1.3. 使用Bcrypt算法处理加密
一般来说,在使用Spring Security时,推荐使用Bcrypt算法对密码进行加密处理!
关于Bcypt算法的使用:
@Test
void bcryptTests() {
String password = "1234";
String encodePassword = new BCryptPasswordEncoder().encode(password);
System.out.println("[bcrypt] encode password=" + encodePassword);
}
如果多次运行,会发现每次运算结果都是不相同的(原文不变,密文一直都不同):
1.[bcrypt] encode password=$2a
10
10
10VUojUDNHcHGRsa/VF8sLyuDEKRPnMOW29hmDud0TjrDK7YwpWE9H6
2.[bcrypt] encode password=$2a
10
10
10wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO
3.[bcrypt] encode password=$2a
10
10
10twSBjL8sgGooL.a0kHjXjuQ9WcamDxsXN2fHR0B0tuIIfXY350I4G
即使对于同一个原文可以运算出N个不同的密文,在使用时,将任何一个密文存储到数据库中,然后将正确的原文提交给Spring Security后,Spring Security就可以完成“登录成功”的验证,当然,如果交给Spring Security的原文是错误的,则会“登录失败”,整个验证过程交给Spring Security框架去完成即可。
1.4. Spring Security登录验证
当Spring Boot项目集成了Spring Security的依赖后,默认情况下,所有的访问都是需要登录的!例如访问此前已经完成的注册功能时 http://localhost:8080/api/v1/users/student/register?phone=13100131001&password=1234&inviteCode=JSD1912-876840,会自动重定向到 http://localhost:8080/login:
以上登录页面的URL及页面都是由Spring Security框架内置的!
在使用Spring Security时,默认的用户名是user
,默认的密码是启动项目时日志中提示的密码:
在Spring Security提供的登录页面中,会验证用户名与密码是否正确,如果登录失败,会提示:
如果登录成功,会自动重定向到登录之前尝试访问的页面,例如:在未登录时,直接访问“用户注册”,会因为未登录被重定向到“登录页面”,当登录成功后,会自动重定向到此前尝试访问的“用户注册”页面!
1.5. 在配置文件中配置Spring Security的登录账号
可以在application.properties
中添加配置,定义登录Spring Security的用户名和密码:
# Spring Security临时使用的用户名和密码
spring.security.user.name=user
spring.security.user.password=1234
当添加以上配置后,再次重启项目,可以看到启动时不再提供临时密码,并且,登录时,输入以上配置的用户名和密码即可!
1.6. 通过内存配置Spring Security的登录账号
如果需要通过程序代码来指定登录的用户名和密码,需要自定义类,该类需要继承自WebSecurityConfigurerAdapter
类:
package cn.tedu.straw.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* <p>创建“密码加密器”对象交给Spring框架进行管理!</p>
* <p>后续,Spring Security将吃通过自动装配的机制得到这个密码加密器!</p>
*
* @return 密码加密器对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中授权
auth.inMemoryAuthentication()
// withUser > 指定用户名
.withUser("root")
// password > 指定密码
.password("1234")
// 指定授权
.authorities("/admin/list");
}
}
则重新启动项目,就可以通过root
/ 1234
来登录了!
1.7. 关于授权-1
关于以上“授权”,配置的值是“权限标识”,是一个自定义的字符串(通常会写成URL路径的格式,但是,也可以随意写成其它格式),表示该用户具有哪些权限,如果需要验证权限,需要在配置类之前添加@EnableGlobalMethodSecurity
注解,并显式的配置注解属性prePostEnabled
的值为true
(该属性默认值为false
):
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* <p>创建“密码加密器”对象交给Spring框架进行管理!</p>
* <p>后续,Spring Security将吃通过自动装配的机制得到这个密码加密器!</p>
*
* @return 密码加密器对象
*/
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中授权
auth.inMemoryAuthentication()
// withUser > 指定用户名
.withUser("root")
// password > 指定密码
.password("1234")
// authorities > 指定授权,方法参数值是权限标识字符串
.authorities("/admin/list");
}
}
然后,可以写一个测试类,测试权限限制:
@RestController
public class TestController {
@PreAuthorize("hasAuthority('/admin/list')")
@GetMapping("/admin/list")
public String adminList() {
return "admin list";
}
@PreAuthorize("hasAuthority('/admin/delete')")
@GetMapping("/admin/delete")
public String adminDelete() {
return "admin delete";
}
}
以上在方法的声明之前添加@PreAuthorize
注解,表示执行该方法之前需要授权,注解参数中的hasAuthority
是固定的名称,括号内的/admin/list
、/admin/delete
就是权限标识,此前配置的用户信息中有/admin/list
,所以,该用户登录后可以访问以上测试控制器中的第1个路径,但是,无权访问第2个路径,当尝试访问第2个路径时,会出现403
错误,表示“权限不足”!
当然,也可以在配置用户信息时,为用户添加若干个权限,例如:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在内存中授权
auth.inMemoryAuthentication()
// withUser > 指定用户名
.withUser("root")
// password > 指定密码
.password("1234")
// authorities > 指定授权,方法参数值是权限标识字符串
.authorities("/admin/list", "/admin/delete");
}
1.8. 关于授权-2
在演示第2种授权的做法之前,应该先将以上案例中的代码使之失效!先将类改名,并在类的声明之前添加@Deprecated
注解,表示“声明为已过期”,同时,将关键的注解注释掉,则当前类就缺失了关键注解,导致项目不会自动使用当前类的各种配置!
在TestController
中,去掉原有的@PreAuthorize
注解,添加更多的请求路径,以便于演示效果:
```java
@RestController
public class TestController {
@GetMapping("/admin/list")
public String adminList() {
return "admin list";
}
@GetMapping("/admin/delete")
public String adminDelete() {
return "admin delete";
}
@GetMapping("/user/list")
public String userList() {
return "user list";
}
@GetMapping("/user/delete")
public String userDelete() {
return "user delete";
}
}
由于此前演示的配置类已经作废,需要重新创建配置类,用于指定用户登录的账号及权限,并且,由于以上控制器类中的方法没有@PreAuthorize
注解了,授权过程也应该在新的配置类中完成:
package cn.tedu.straw.api.config;
import org.springframework.context.annotation.Bean;
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;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 当前方法的主要作用是:授权
auth.inMemoryAuthentication()
.withUser("admin")
.password("1234")
.authorities("admin_list", "admin_delete", "user")
.and()
.withUser("user")
.password("1234")
.authorities("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 当前方法的主要作用是:访问控制
http.authorizeRequests()
// 使用antMatchers()配置需要管理权限的URL,可以使用通配符,?表示任何1个字符,*表示任何1层路径资源,**表示任何层次的资源
// 例如:配置为 /user/* 可以匹配 /user/delete、/user/list,却不可以匹配 /user/2020/list
// 例如:配置为 /user/** 可以配置 /user/delete、/user/list、/user/2020/list、/user/2020/08/list
// 紧随其后,使用hasAuthority()配置权限标识
.antMatchers("/admin/list").hasAuthority("admin_list")
.antMatchers("/admin/delete").hasAuthority("admin_delete")
.antMatchers("/user/**").hasAuthority("user")
// 对任何请求进行授权检查
.anyRequest().authenticated();
// 验证权限时,是使用登录表单进行授权的
http.formLogin();
}
}
在演示案例中,当需要退出登录时,可以在浏览器输入 http://localhost:8080/logout 以退出登录并切换账号进行测试。
以上2种做法各有优点,在“授权-1”的作法中,优点主要是对应关系非常明确,缺点在于不可以使用通配符,在“授权-2”的做法,优点主要在于可以集中管理权限(代码都写在同一个方法中),并可以使用路径中的通配符,但是,也因为管理很集中,就导致在控制器的代码中,权限检查并不直观。
1.9. 在密文之前添加前缀标识加密时使用的算法
在Spring Security处理密码时,可以不指定密码加密器(去除自动装配的任何密码加密器),然后,在密文之前使用{}
框住密码加密器的id,例如:
{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO
则Spring Security在处理密码时,就会自动根据{bcrypt}
对应的密码加密器(BCryptPasswordEncoder
)来处理密码!
完整示例如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 当前方法的主要作用是:授权
auth.inMemoryAuthentication()
.withUser("admin")
.password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
.authorities("admin_list", "admin_delete", "user")
.and()
.withUser("user")
.password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
.authorities("user");
}
当然,以上代码等效于(以下代码显式的指定了密码加密器,同时密文没有指定密码加密器的id):
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 当前方法的主要作用是:授权
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
.authorities("admin_list", "admin_delete", "user")
.and()
.withUser("user")
.password("$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
.authorities("user");
}
1.10. 基于UserDetails的用户登录
以上的做法中,都是将用户的用户名、密码记录在配置类中,而配置类是整个项目启动之初就会被加载的,所以,这种做法无法满足动态用户名、密码的验证,在实际案例中,用户名、密码都应该是从数据库中读取出来的,不可能在项目启动之初就读取所有用户的用户名、密码并作为配置信息!
在Spring Security中,定义了一个UserDetailsService
的接口,可以自定义类实现该接口:
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return null;
}
}
在整个登录过程中,Spring Security可以自动调用以上接口实现类中的loadUserByUsername()
方法,并给出登录时的用户名,要求开发人员返回该用户名匹配的用户信息UserDetails
,至少需要包括用户的密码,然后,Spring Security就可以根据返回信息中所包含的密码进行密码的验证,在整个操作过程中,Spring Security既不会给出用户此次登录的原始密码,也不要求以上方法返回的UserDetails
中包含原始密码,而是全程自动处理!
演示代码示例:
package cn.tedu.straw.api.security;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 假设仅chengheng是正确的用户名
if ("chengheng".equals(username)) {
// 用户名是chengheng,则返回该用户的信息,后续Spring Security将根据返回的信息完成登录验证及授权
UserDetails userDetails = User.builder()
.username("chengheng")
.password("{bcrypt}$2a$10$wSQgf2fQn6bnWP1ASXgw3uOlwmgW1/UtqczahAoaqpYmXPiBFJQrO")
.authorities("user")
.build();
return userDetails;
}
// 用户名不是root,则返回null,表示”无此用户“
return null;
}
}
然后,还需要在WebSecurityConfigureAdapter
的子类中重写configureation(AuthenticationManagerBuilder auth)
方法:
@Resource
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 当前方法的主要作用是:登录验证,授权
auth.userDetailsService(userDetailsService);
}
2. 注册时对密码进行加密处理
由于使用了Spring Security框架,无论请求哪个路径,都是需要事先登录的,这个设计是不合理的,会在后续开发“登录”时再解决!
目前,在业务层中,尚未对用户注册时填写的密码进行加密,则需要修改“学生注册”的业务,在处理过程中,取出用户提交的原始密码,基于原始密码执行加密处理,然后将密文存储到数据库中!
为了避免在业务层使用Spring Security包中的工具类,导致后续各层代码之间的耦合度较高、多层代码都需要依赖于Spring Security,应该将“执行密码加密”的功能封装到专门的类中,使得业务层并不直接依赖于Spring Security!所以,在util
包(需创建)中创建PasswordUtils
工具类,在工具类添加“执行密码加密”的方法:
package cn.tedu.straw.api.util;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 密码工具类
*/
public class PasswordUtils {
/**
* 密码加密器
*/
private static final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
/**
* 执行密码加密
*
* @param rawPassword 原密码
* @return 加密后得到的密文
*/
public static String encode(String rawPassword) {
return "{bcrypt}" + passwordEncoder.encode(rawPassword);
}
}
```然后,在`UserServiceImpl`的`regStudent()`方法补充:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200826124038548.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xldWtl,size_16,color_FFFFFF,t_70#pic_center)
完成后,由于修改了业务层代码,需要再次执行业务层测试,以保证刚才的调整是正确的!
当业务层测试通过后,还应该**再次重启项目**,在浏览器中通过 http://localhost:8080/api/v1/users/student/register?phone=13100135001&password=1234&inviteCode=JSD1912-876840&nickname=Hello 进行测试注册。