A place to hold mainly reading notes, and some technical stuff occasionally. 这里主要是一些读书笔记、感悟;还有部分技术相关的内容。
目录[-]
本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity
的各项Feature。
第一步,新建一个SpringBoot
项目,起名:springboot-security
,核心依赖为Web
,此处先不引入SpringSecurity
依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
建好项目后,创建一个简单的HelloController.java,包含一个/hello
的GET
请求:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello springsecurity";
}
}
好了,这时直接启动项目,在浏览器访问:localhost:8080/hello
返回hello springsecurity
,表明接口正常。
pom文件中引入SpringSecurity
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
然后不做其他任何改动,直接重启项目。
在浏览器访问:localhost:8080/hello
,这次,接口返回302,跳转到了http://localhost:8080/login
,要求我们输入用户名与密码完成登录。可问题是,这里的账号密码分别是多少呢?
仔细观察日志,在控制台的启动日志中有如下一行:
Using generated security password: 26420b20-8ab1-421a-968b-2c537e420527
这表明SpringSecurity
为我们生成了一个UUID
形式的密码,默认的用户名为user
;输入用户名与密码,成功登录后,可以正常访问/hello
。
因此,仅引入SpringSecurity
依赖,就实现了对我们后端接口的防护,这便是使用框架的意义:简单、直接、有效。
问题来了,鬼知道它的默认用户名是user
。。这个可用从官网https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/#servlet-hello-auto-configuration或者SpringBoot
自动配置类的源码查到。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.boot.autoconfigure.security;
import ...
@ConfigurationProperties(
prefix = "spring.security"
)
public class SecurityProperties {
public static final int BASIC_AUTH_ORDER = 2147483642;
public static final int IGNORED_ORDER = -2147483648;
public static final int DEFAULT_FILTER_ORDER = -100;
private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
private final SecurityProperties.User user = new SecurityProperties.User();
public SecurityProperties() {
}
public SecurityProperties.User getUser() {
return this.user;
}
public SecurityProperties.Filter getFilter() {
return this.filter;
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList();
private boolean passwordGenerated = true;
...
}
...
}
Note: 最开始引入了devtools
依赖,目的是能够在配置、代码更新时,能够热启动项目,不用每次都手动停止,再启动,不过在Idea
中需要进行如下两步,完成配置:
Ctrl+Shift+Alt+/
打开maintenance
面板,选择第一个registry
, 勾选compiler.automake.allow.when.app.running
保存;Ctrl+Alt+S , 打开配置面板,在File |
Settings | Build, Execution, Deployment | Compiler下勾选Build project automatically ; |
我们知道,在application.properties
或者application.yml
中,可以进行一些自定义配置,对于SpringSecurity
也不例外。这里以application.properties
为例,在其中添加如下配置:
spring.security.user.name=ok
spring.security.user.password=000
以上配置用户名为ok,密码为000;为了直接看到效果,我们在浏览器中清除本站的Cookie,或者访问SpringSecurity为我们提供的/logout
端点,退出登录。
输入实验1中的用户名user
,密码26420b20-8ab1-421a-968b-2c537e420527
,发现无法完成登录;
输入application.properties
中配置的用户名ok
,密码000
,登录成功,并实现对/hello
端点的访问。
Note:其实,还可以通过UserDetailsManager
或InMemoryUserDetailsManager
在内存中配置用户名与密码,实现对配置文件中账号信息的覆盖。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("dev").password("123").authorities("p1").build());
return manager;
}
}
上面这段代码干了这样几件事:
SecurityConfig
,继承自WebSecurityConfigurerAdapter
;InMemoryUserDetailsManager
创建用户时,指定了用户名、密码、权限(或角色);重启项目,浏览器访问:localhost:8080/hello
,跳转到http://localhost:8080/login
;键入用户名dev
,密码123
,后台报错了:没有密码编码器。。
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
作为一个安全框架,SpringSecurity还是很有原则的,要求使用者必须对密码进行加密,因为数据泄露的事件在历史上也是多次发生了。。而且明文存储密码,还可能被偷窥;一旦持久化的数据库暴露,会引发一系列的其他连环事故:比如撞库(讲真,大多数人为了方便记忆,在各网站、平台的密码都是相同的,大家都是肉体凡胎,谁又能记住不同的密码呢),怕了怕了。
解决方法1:我们修改配置,定义一个Bean,返回一个PasswordEncoder
实例,
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance(); // Deprecated
}
Note:不过上面这个Bean中的NoOpPasswordEncoder
被标记为Deprecated
,并且从注释中可以看到,建议这种密码编码器仅作为遗留项目或测试使用,因为作为一个密码编码器,它的matches
方法其实什么也没干⊙︿⊙
/**
* This {@link PasswordEncoder} is provided for legacy and testing purposes only and is
* not considered secure.
*
* A password encoder that does nothing. Useful for testing where working with plain text
* passwords may be preferred.
*
* @author Keith Donald
* @deprecated This PasswordEncoder is not secure. Instead use an adaptive one way
* function like BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or SCryptPasswordEncoder.
* Even better use {@link DelegatingPasswordEncoder} which supports password upgrades.
* There are no plans to remove this support. It is deprecated to indicate that this is a
* legacy implementation and using it is considered insecure.
*/
@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {
private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();
private NoOpPasswordEncoder() {
}
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.toString().equals(encodedPassword);
}
/**
* Get the singleton {@link NoOpPasswordEncoder}.
*/
public static PasswordEncoder getInstance() {
return INSTANCE;
}
}
解决方法2:这里我们直接在密码处进行编码:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
manager.createUser(User.withUsername("dev").password(encoder.encode("123")).authorities("p1").build());
return manager;
}
}
上述代码增加了编码器,通过PasswordEncoderFactories
的工厂方法创建一个PasswordEncoder
实例,实现密码加密;
再次登录,输入用户名dev
, 密码123
即可,实现对/hello
端点的访问。
@Configuration
@Slf4j
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
log.info("Password: {}", encoder.encode("123"));
manager.createUser(User.withUsername("dev").password(encoder.encode("123")).authorities("p1").build());
return manager;
}
}
上面代码增加了一行输出,打印加密后的密码:Password: {bcrypt}$2a$10$4wUnbQvRHsxNuD1MTxdhru7GPANsBh/0Y37fepAduOGQsmtQrMjs.
,可以看到输出的密码前有个特殊说明{bcrypt},表明密码的加密方式为bcrypt
,也可以从PasswordEncoderFactories
类中查看默认的密码加密方式为bcrypt
。bcrypt
密码结构见下图:
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
@SuppressWarnings("deprecation")
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256",
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
既然能够作为SpringSecurity
默认的密码加密方式,bcrypt
这种加密方法应当是足够安全的,关于bcrypt
与其他加密算法对比的更多信息可参阅这篇10年前的文章:https://codahale.com/how-to-safely-store-a-password/
If you have any questions or any bugs are found, please feel free to contact me.
Your comments and suggestions are welcome!