1-初识SpringSecurity:user in-memory

简介: 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…


目录
相关文章
|
5月前
|
SQL JSON 前端开发
较为完整的SpringBoot项目结构
本文介绍了SpringBoot项目的分层结构与目录组成。项目分为四层:**controller层**(前端交互)、**service层**(业务逻辑处理)、**dao层**(数据库操作)和**model层**(实体类定义)。分层设计旨在实现关注点分离,降低耦合度,提高系统灵活性、可维护性和扩展性。此外,还详细说明了项目目录结构,包括`controller`、`service`、`dao`、`entity`、`param`、`util`等子目录的功能划分,便于团队协作开发。此架构有助于前后端分离,明确各模块职责,符合高内聚低耦合的设计原则。
3631 1
Unity精华☀️Audio Mixer终极教程:用《双人成行》讲解它的用途
Unity精华☀️Audio Mixer终极教程:用《双人成行》讲解它的用途
|
存储 移动开发 前端开发
ruoyi-nbcio-plus基于多租户的flowable设计考虑
ruoyi-nbcio-plus基于多租户的flowable设计考虑
565 1
|
SQL Cloud Native 关系型数据库
云原生数据仓库使用问题之控制JDBC方式请求的SQL大小限制的参数是什么
阿里云AnalyticDB提供了全面的数据导入、查询分析、数据管理、运维监控等功能,并通过扩展功能支持与AI平台集成、跨地域复制与联邦查询等高级应用场景,为企业构建实时、高效、可扩展的数据仓库解决方案。以下是对AnalyticDB产品使用合集的概述,包括数据导入、查询分析、数据管理、运维监控、扩展功能等方面。
|
JavaScript Java 测试技术
基于ssm+vue.js+uniapp小程序的猎头公司管理系统附带文章和源代码设计说明文档ppt
基于ssm+vue.js+uniapp小程序的猎头公司管理系统附带文章和源代码设计说明文档ppt
187 1
|
机器学习/深度学习 人工智能 数据处理
一文速通自监督学习(Self-supervised Learning):教机器自我探索的艺术
一文速通自监督学习(Self-supervised Learning):教机器自我探索的艺术
2645 0
leetcode78子集刷题打卡
leetcode78子集刷题打卡
93 0
|
C++
Visual C++ Build Tools 2015 安装包丢失或损坏[解决方案]
Visual C++ Build Tools 2015 安装包丢失或损坏[解决方案]
1420 0