1-初识SpringSecurity:user in-memory

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 1-初识SpringSecurity:user in-memory

背景


本系列教程,是作为团队内部的培训资料准备的。主要以实验的方式来体验SpringSecurity的各项Feature。


实验0:Hello SpringSecurity


第一步,新建一个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,包含一个/helloGET请求:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(){
        return "hello springsecurity";
    }
}

好了,这时直接启动项目,在浏览器访问:localhost:8080/hello返回hello springsecurity,表明接口正常。


实验1:默认用户名与密码


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。。这个可用从官网docs.spring.io/spring-secu…或者SpringBoot自动配置类的源码查到。

image.png

//
// 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中需要进行如下两步,完成配置:


  1. Ctrl+Shift+Alt+/打开maintenance面板,选择第一个registry, 勾选compiler.automake.allow.when.app.running保存;


  1. Ctrl+Alt+S, 打开配置面板,在File | Settings | Build, Execution, Deployment | Compiler下勾选Build project automatically


实验2:配置文件中覆盖默认的用户名与密码


我们知道,在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:其实,还可以通过UserDetailsManagerInMemoryUserDetailsManager在内存中配置用户名与密码,实现对配置文件中账号信息的覆盖。


实验3:内存中覆盖默认的用户名与密码


@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;
     }
}

上面这段代码干了这样几件事:


  1. 自定义配置类SecurityConfig,继承自WebSecurityConfigurerAdapter


  1. 定义一个Bean:userDetailsService,其中创建并返回了一个in-memory的用户;


  1. 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端点的访问。


实验4:密码编码器的默认加密方式


@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类中查看默认的密码加密方式为bcryptbcrypt密码结构见下图:

image.png

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年前的文章:codahale.com/how-to-safe…


目录
打赏
0
0
0
0
8
分享
相关文章
shiro学习之错误 No realms have been configured! One or more realms must be present to execute an authori
shiro学习之错误 No realms have been configured! One or more realms must be present to execute an authori
javax.security.auth.login.LoginException: Unable to obtain password from user
假设我们公司有自己的门户网站,现在我们收购了一家公司,他们数据库采用ldap存储用户数据,那么为了他们账户能登陆我们公司项目所以需要集成,而不是再把他们的账户重新在mysql再创建一遍,万一人家有1W个账户呢,不累死了且也不现实啊。我之所以选这么旧的版本,是因为我最后要在自己项目集成,我们项目就是上面版本附近的,所以不能选太高版本,这点请注意各版本之间的兼容性问题。:如果里面的某些配置不知道在哪或者不知道干啥的,可以看我的前面的博客,详细介绍了安装配置等,可以大致了解参数。
154 9
spring-boot-actuator报错Full authentication is required to access this resource
spring-boot-actuator报错Full authentication is required to access this resource
4809 0
github报错(完美解决):获取token。remote: Support for password authentication was removed on August 13, 2021.
这篇文章介绍了如何在GitHub上解决因密码认证被移除而导致的推送错误,通过创建和使用个人访问令牌(token)来代替密码进行身份验证。
756 0
IDEA添加Swagger2:Parameter 0 of method linkDiscoverers in org. springframework hateoas.config.Hateoasconfiguration required a single bean, but 15 were found:
IDEA添加Swagger2:Parameter 0 of method linkDiscoverers in org. springframework hateoas.config.Hateoasconfiguration required a single bean, but 15 were found
|
8月前
|
Springboot用JUnit测试接口时报错Failed to determine a suitable driver class configure a DataSource: ‘url‘
Springboot用JUnit测试接口时报错Failed to determine a suitable driver class configure a DataSource: ‘url‘
154 0
Gitlab报错:No authentication methods configured on login page
Gitlab报错:No authentication methods configured on login page
186 0
【elementUI + Spring报错解决方案】Required request part ‘*****‘ is not present
【elementUI + Spring报错解决方案】Required request part ‘*****‘ is not present
1096 0
【elementUI + Spring报错解决方案】Required request part ‘*****‘ is not present
         User root is not allowed to impersonate anonymous                     
错误: bymain is not allowed to impersonate hadoop(或者          User root is not allowed to impersonate anonymou...
1766 0
解决sprinboot项目连接数据库出现Access denied for user ‘‘@‘localhost‘ (using password: NO)
解决sprinboot项目连接数据库出现Access denied for user ‘‘@‘localhost‘ (using password: NO)
解决sprinboot项目连接数据库出现Access denied for user ‘‘@‘localhost‘ (using password: NO)
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等