日积月累,水滴石穿 😄
前言
如果想让我们的接口受到保护,只需要在项目中加入 spring-boot-starter-security
依赖。
导入依赖
具体依赖版本
boot-starter-parent = 2.3.12.RELEASE
boot-starter-security = 2.3.12.RELEASE
security-web = 5.3.9.RELEASE
security-config = 5.3.9.RELEASE
pom 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>cn.cxyxj</groupId>
<artifactId>security-study-01</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
效果
依赖导入之后,我们来看看效果,在项目中提供一个 hello
接口。
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "你好 Spring Security";
}
}
项目启动成功之后,可以在浏览器输入我们的接口地址:localhost:8080/hello
。
发起请求之后,并没有调用 hello
接口,而是来到了security
默认的登录页面:
这个页面所处的源码位置为:DefaultLoginPageGeneratingFilter#generateLoginPageHtml。
那登录用户名和登录密码是什么呢? 在我们的项目启动过程中,在日志中会打印一段话:
INFO 37204 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: df563e43-4b0c-4760-9dd3-eb512b591d7b
这个就是 Spring Security 为默认用户 user 生成的临时密码。密码每次启动都会改变。
各位小伙伴可能会有疑问,为什么你知道默认用户名是 user,密码为什么每次会改变呢?
各位小伙伴可以发现,该日志打印时所处的类为 UserDetailsServiceAutoConfiguration
。
在该类中有个方法 getOrDeducePassword
,当 isPasswordGenerated
方法返回 true
,就会打印如上日志。
private String getOrDeducePassword(User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}
我们进行跟踪 isPasswordGenerated
方法,来到了 SecurityProperties
类中,发现 passwordGenerated
属性默认即为 true
,当然还有额外发现。
可以看到,默认的用户名就是 user,密码则是 UUID 字符串。各位小伙伴现在了解了吧!OK,让我们回归正题。
当我们输入用户名密码之后,点击按钮,就会登录成功,会跳转访问 /hello
接口。
自定义账号密码
上面讲到这个密码每次启动都会改变,每次都需要去控制台寻找,这非常的不方便。那有没有方法固定密码呢?
对用户名/密码进行配置,有三种不同的方式:
- 基于配置文件 properties 或者 yml
- 基于 UserDetailsService 接口
- 基于配置类 WebSecurityConfigurerAdapter
配置文件
通过前文可以知道Security
的配置是放在SecurityProperties
类中。而SecurityProperties
就是一个读取配置文件的类,定义如下:
@ConfigurationProperties(
prefix = "spring.security"
)
public class SecurityProperties {
各位小伙伴对@ConfigurationProperties
注解肯定不陌生吧!所以自定义用户名和密码只需在配置文件添加如下配置即可:
spring.security.user.name=admin
spring.security.user.password=123
上述就是我们自定义的用户名和密码。
那为什么可以这样配置? 还是在UserDetailsServiceAutoConfiguration
类中,默认情况下,UserDetailsServiceAutoConfiguration
类,会创建一个内存级别的 InMemoryUserDetailsManager
对象,其中包含用户名为 user,密码为 UUID 随机的用户 。而我们添加 spring.security.user 配置项之后,UserDetailsServiceAutoConfiguration
会基于配置的信息在内存中创建一个用户。
配置好了之后,重新启动,控制台不会再打印随机生成的密码了。启动成功后,我们可以使用配置文件中的账号、密码登录。
基于UserDetailsService接口
这种方式是在实际开发中使用最多的一种方式!可以由开发者自由控制用户的来源,比如:从数据库加载。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserDetails userDetails = User.withUsername("cxyxj")
.password("{noop}1234").authorities("admin").build();
return userDetails;
}
}
withUsername 就是账号,password 就是密码,authorities 就是角色。在 security 中用户信息都会包装为一个 UserDetails 对象。
Spring Security5 中新增加了加密方式,并把原有的 Spring Security 的密码存储格式改了,修改后的密码存储格式为:{id}pwd
。
如果不写{id},在发起登录请求时,会出现以下异常:
支持的加密方式可以通过 PasswordEncoderFactories 查看。
也可以通过
PasswordEncoder
配置指定加密方式。
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
UserDetails userDetails = User.withUsername("cxyxj")
.password("1234").authorities("admin").build();
return userDetails;
}
当然 Spring Security 官方推荐的加密方式是 BCrypt。
@Bean
public PasswordEncoder passwordEncoder(){
// return NoOpPasswordEncoder.getInstance();
return new BCryptPasswordEncoder();
}
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
String encode = passwordEncoder.encode("12345");
UserDetails userDetails = User.withUsername("cxyxj")
.password(encode).authorities("admin").build();
return userDetails;
}
基于配置类WebSecurityConfigurerAdapter
首先创建一个 Spring Security
的配置类,继承 WebSecurityConfigurerAdapter
类。
@Configuration
//@EnableWebSecurity 如果你的项目是 SpringBoot 项目,该注解就没必要写
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
是否需要添加@EnableWebSecurity
注解?如果在项目中引入的是spring-boot-starter-security
依赖则不需要添加 @EnableWebSecurity
,可以参考自动配置类: spring-boot-autoconfigure-x.x.x.RELEASE.jar!/META-INF/spring.factories
下的 SecurityAutoConfiguration
。
如果是单独引入 spring-security-config 和 spring-security-web 依赖,则需要添加 @EnableWebSecurity注解。
WebSecurityConfigurerAdapter
类是个适配器,如果我们需要自定义配置,那就要继承它。比如基于内存的认证,当然添加认证的方式还有很多,但是万变不离其宗,都是往 Security
中添加一个 UserDetails
对象 ,配置方式如下:
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("cxyxj")
.password("123").roles("admin","user")
.and()
.withUser("security")
.password("security").roles("user");
}
-
自定义 SecurityConfig 继承自 WebSecurityConfigurerAdapter,重写
configure(AuthenticationManagerBuilder auth)
方法。AuthenticationManagerBuilder
用于创建AuthenticationManager
认证管理器。 比如:内存身份验证,LDAP身份验证,JDBC的身份验证,还可以可以添加UserDetailsService
以及添加AuthenticationProvider
。 -
提供了一个 PasswordEncoder 的实例,类型为 NoOpPasswordEncoder 实例,表示不给密码进行加密 。
-
在 configure 方法中, inMemoryAuthentication 表示开启在内存中定义用户,withUser 定义用户名,password 定义用户密码,roles 是用户角色。
在使用该方式之前,先把 UserDetailsServiceImpl 类中的代码先全部注释。因为两个类中都注入了 PasswordEncoder 实例。当然,你也可以将代码修改。
当然也可以重写WebSecurityConfigurerAdapter#userDetailsService()
方法或者 WebSecurityConfigurerAdapter#userDetailsServiceBean()
方法,并通过 @Bean 交给 Spring 管理
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("cxyxj")
.password("123").roles("admin","user")
.and()
.withUser("security")
.password("security").roles("user");
// 这句话要加
auth.userDetailsService(userDetailsService());
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username -> new User("hhhhh", "12345",
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
注意:基于UserDetailsService
接口配置用户名密码时,这种方式需要注意一点,如果你同时继承了 WebSecurityConfigurerAdapter
类并重写了 configure(AuthenticationManagerBuilder auth)
方法,则需要手动设置 UserDetailsService
的来源 auth.userDetailsService(userService)
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。