现在,我们已经对 Spring Security 体系结构及其核心类进行了高层次的概述,接下来让我们更深入地研究一两个核心接口及其实现,特别是
Spring Security 中的默认实现称为
如果使用命名空间,将在内部创建和维护 <bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager"> <constructor-arg> <list> <ref local="daoAuthenticationProvider"/> <ref local="anonymousAuthenticationProvider"/> <ref local="ldapAuthenticationProvider"/> </list> </constructor-arg> </bean>
在上面的例子中,我们有三个提供者。它们按照所示的顺序(使用
向
默认情况下(从 Spring Security 3.1 开始),
例如,在使用用户对象缓存时,这可能会导致在无状态应用程序中提高性能。如果
Spring Security 实现的最简单的 <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="userDetailsService" ref="inMemoryDaoImpl"/> <property name="passwordEncoder" ref="passwordEncoder"/> </bean>
如本参考指南前面所述,大多数身份验证提供者都利用 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的
由于
很容易使用创建自定义 <user-service id="userDetailsService"> <!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that NoOpPasswordEncoder should be used. This is not safe for production, but makes reading in samples easier. Normally passwords should be hashed using BCrypt --> <user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" /> <user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" /> </user-service> 这也支持使用外部属性文件: <user-service id="userDetailsService" properties="users.properties"/> 属性文件应该包含表单中的条目 username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] 例如 jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled bob=bobspassword,ROLE_USER,enabled
Spring Security 还包括可以从 JDBC 数据源获得身份验证信息的 <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/> <property name="username" value="sa"/> <property name="password" value=""/> </bean> <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> <property name="dataSource" ref="dataSource"/> </bean>
可以通过修改上面所示的
默认情况下,
Spring Security 的 多年来,存储密码的标准机制已经发展。开始时密码以明文形式存储。密码被认为是安全的,因为数据存储的密码保存在所需的凭证中以访问密码。然而,恶意用户能够找到使用 SQL 注入等攻击获取用户名和密码的大量数据转储的方法。随着越来越多的用户凭证成为公共安全问题,意识到我们需要做更多的工作来保护用户密码。 然后鼓励开发人员在通过单向散列(例如 SHA-256)运行密码后存储密码。当用户试图进行身份验证时,将哈希密码与它们键入的密码的哈希值进行比较。这意味着系统只需要存储口令的单向散列。如果发生了攻破,则只有密码的单向散列被暴露。由于散列是单向的,而且在计算上很难猜测给定散列的密码,因此不值得努力找出系统中的每个密码。为了击败这个新系统,恶意用户决定创建被称为彩虹表的查找表。不是每次都做猜测每一个密码的工作,而是计算一次密码并将其存储在查找表中。 为了减少彩虹表的有效性,鼓励开发人员使用加盐密码。代替只使用密码作为哈希函数的输入,将为每个用户的密码生成随机字节(称为加盐)。通过生成的哈希函数运行盐和用户密码。盐将与用户密码一起存储在明文中。然后,当用户试图进行身份验证时,将散列密码与存储的盐的散列和他们键入的密码进行比较。独特的加盐意味着彩虹表不再有效,因为哈希对于每个加盐和密码组合都不同。 在现代,我们认识到密码散列(如 SHA-256)不再安全。原因是,用现代硬件,我们可以一秒钟执行数十亿的散列计算。这意味着我们可以轻松地破解每个密码。 现在鼓励开发人员利用自适应单向函数来存储密码。使用自适应单向函数验证密码是有意的资源(即 CPU、内存等)密集的。一个自适应单向函数允许配置一个工作因子,它可以随着硬件变得更好而增长。建议将工作因子调整到大约1秒以验证系统上的密码。这种折衷是让攻击者很难破解密码,但不会花费太大,这会给您自己的系统带来过多的负担。Spring Security 试图为工作因素提供一个良好的起点,但是鼓励用户为自己的系统定制工作因素,因为性能会随着系统而急剧变化。应该使用的自适应单向函数的例子包括 bcrypt、PBKDF2、scrypt 和 Argon2。 由于自适应单向函数故意占用大量资源,因此为每个请求验证用户名和密码将显著降低应用程序的性能。Spring Security(或任何其它库)都无法加速密码验证,因为通过使验证资源密集来获得安全性。鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即 session、OAuth Token 等)。短期凭证可以快速验证而不损失任何安全性。
在 Spring Security 5.0 之前,默认
相反,Spring Security 引入了
你可以很容易地使用 PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); 或者,你可以创建自己的自定义实例。例如: String idForEncode = "bcrypt"; Map encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); encoders.put("noop", NoOpPasswordEncoder.getInstance()); encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); encoders.put("scrypt", new SCryptPasswordEncoder()); encoders.put("sha256", new StandardPasswordEncoder()); PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders); 密码的一般格式是: {id}encodedPassword
这样,id 是用于查找应该使用哪个 {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
传递到构造函数的 {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
匹配是基于
通过使用 如果你把一个演示或一个示例放在一起,就需要花费一些时间来散列用户的密码。有方便的机制,使这更容易,但这仍然是不打算用于生产。 User user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("user") .build(); System.out.println(user.getPassword()); // {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 如果你正在创建多个用户,还可以重用生成器。 UserBuilder users = User.withDefaultPasswordEncoder(); User user = users .username("user") .password("password") .roles("USER") .build(); User admin = users .username("admin") .password("password") .roles("USER","ADMIN") .build(); 这会对存储的密码进行哈希,但密码仍然在内存中和编译后的源代码中公开。因此,它仍然不被认为是安全的生产环境。对于生产,你应该在外部散列你的密码。 当存储的一个密码没有 id 时,会发生以下错误,如称为密码存储格式的部分所述。 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
解决错误的最简单方法是切换到显式地提供密码被编码的
@Bean public static NoOpPasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); }
如果使用 XML 配置,则可以用 id <b:bean id="passwordEncoder" class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
或者,你可以用正确的 id 将所有密码前缀,并继续使用 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 到 {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 对于映射的完整列表,请参阅 PasswordEncoderFactories 的 JavaDoc。
// Create an encoder with strength 16 BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result)); Spring Security 增加了 Jackson 支持持久性 Spring Security 相关类的支持。当使用分布式会话(即会话复制、Spring Session 等)时,这可以提高序列化 Spring Security 相关类的性能。
要使用它,注册 ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); List<Module> modules = SecurityJackson2Modules.getModules(loader); mapper.registerModules(modules); // ... use ObjectMapper as normally ... SecurityContext context = new SecurityContextImpl(); // ... String json = mapper.writeValueAsString(context); |