SpringSecurity学习(四)密码加密、RememberMe记住我

news2024/11/24 6:39:35

文章目录

  • 密码加密
    • 一、简介
      • 密码为什么要加密
      • 常见的加密解决方案
      • PasswordEncoder详解
      • DelegatingPasswordEncoder
    • 二、自定义加密方式
      • 1. 使用灵活的密码加密方案(BCryptPasswordEncoder)
        • 加密
        • 验证(推荐)需要在密码前指定加密类型`{bcrypt}`
      • 2. 使用固定的密码加密方案(BCryptPasswordEncoder)
    • 三、密码自动升级
      • 实现UserDetailsPasswordService接口的updatePassword方法
  • RememberMe记住我
    • 一、简介
    • 二、基本应用
    • 三、原理分析
      • RememberMeServices
      • 默认使用TokenBasedRememberMeService
      • 总结:
      • RememberMe流程图
    • 四、持久化令牌
      • 实现一:需要手动创建数据库表结构
      • 实现二:这种方式可以自动创建数据库表结构
    • 五、前后端分离开发记住我
      • 编写自定义的MyPersistentTokenBasedRememberMeServices
      • 修改SecurityConfig配置
  • 附:

密码加密

一、简介

密码为什么要加密

密码泄露,多个网站用同一密码。salt加盐。

常见的加密解决方案

  • Hash算法:
    最早使用类似SHA-256、SHA-512、MD5这样的单向Hash算法。用户注册后,数据库保存加密后的字串,当用户输入密码时,进行加密比对。
    但是由于彩虹表攻击以及硬件发展,计算机每秒执行数十亿次hash计算,这意味着及时密码加密加盐也不安全。
  • 单项自适应函数:
    在SpringSecurity中,主推单向自适应函数。这种自适应单向函数在进行密码比对时,会有意占用大量系统资源(CPU、内存等),增加恶意用户攻击难度。
    可以通过bcrypt、PBKDF2、sCrypt、argon2来体验这种自适应单项函数加密。
    在这里插入图片描述

PasswordEncoder详解

通过对认证流程分析,实际的密码比较是由PasswordEncoder完成的,因此只需要使用PasswordEncoder不同的实现来完成的。

public interface PasswordEncoder {
	// 用来进行明文加密
    String encode(CharSequence rawPassword);
	// 用来比较密码
    boolean matches(CharSequence rawPassword, String encodedPassword);
	// 用来对密码进行升级
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

默认提供的加密算法如下:
在这里插入图片描述在这里插入图片描述

DelegatingPasswordEncoder

在SpringSecurity后,默认的密码加密方式为DelegatingPasswordEncoder(一个代理类,而不是加密方案)。主要用来代理上面不同的加密方案。为什么不直接使用加密方案,而采用代理类?

  • 兼容性:使用DelegatingPasswordEncoder可以帮助使用旧密码加密方式的系统顺利迁移到SpringSecurity。允许在同一个系统中同时存在不同加密方案。
  • 便捷性:密码存储方案是可以变化的。使用DelegatingPasswordEncoder作为默认密码加密方案,在需要修改加密方案时,只需要做出小部分改动。

通过源码分析可以知道,如果在工厂类中制定了PasswordEncoder,就会使用PasswordEncoder,否则默认使用DelegatingPasswordEncoder。
SecurityConfigure -> AuthenticationManager -> PasswordEncoder -> DelegatingPasswordEncoder -> PasswordEncoderFactory

二、自定义加密方式

1. 使用灵活的密码加密方案(BCryptPasswordEncoder)

加密

@Test
void test_bcrypt_security() {
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(10);
    String encode = bCryptPasswordEncoder.encode("123");
    System.out.println("encode = " + encode);
}

验证(推荐)需要在密码前指定加密类型{bcrypt}

// 使用PasswordEncoder的第一种方式
@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();
    memoryUserDetailsManager.createUser(User.withUsername("whx")
            .password("{bcrypt}$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());
    return memoryUserDetailsManager;
}

2. 使用固定的密码加密方案(BCryptPasswordEncoder)

// 使用PasswordEncoder的第二种方式
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder(10);
}
@Bean
public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager memoryUserDetailsManager = new InMemoryUserDetailsManager();
    memoryUserDetailsManager.createUser(User.withUsername("whx")
            .password("$2a$10$DAmuV68SQcVTIXf9Pvb3kerV6KSmX6sNNV/o9LUoejrC0A21Bw/m.").roles("admin").build());
    return memoryUserDetailsManager;
}

三、密码自动升级

实现UserDetailsPasswordService接口的updatePassword方法

要实现密码的自动升级,我们只需要实现UserDetailsPasswordService接口中的updatePassword方法即可。

@Component
public class MyUserDetailsService implements UserDetailsService, UserDetailsPasswordService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.getUserByUname(username);
        if (ObjectUtils.isEmpty(user)) {
            throw new UsernameNotFoundException("用户名不正确");
        }
        List<Role> roles = userMapper.getRolesByUid(user.getId());
        user.setRoles(roles);
        return user;
    }

    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        User param = (User) user;
        Integer result = userMapper.updateUnameByName(user.getUsername(), newPassword);
        if (result == 1) {
            param.setPassword(newPassword);
        }
        return param;
    }
}

RememberMe记住我

一、简介

RememberMe记住我。是一种服务端的行为,而不是将用户密码保存在cookie中。传统登录方式是基于Session的,这样一旦用户关闭浏览器重开,就需要再次登录,太过麻烦。
实现思路是通过cookie来记住当前用户身份。当用户登录成功后,通过算法,将用户信息、时间戳加密后通过响应头带回前端存到cookie。当重开浏览器后,会自动将cookie信息发送给服务器进行校验分析。从而确定用户身份。具有时效性,一般的为一周左右。

二、基本应用

  1. 后端开启记住我功能http.rememberMe();
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests().anyRequest().authenticated();
    http.formLogin();
    // 开启记住我功能
    http.rememberMe();
    http.csrf().disable();
    return http.build();
}
  1. 前端自定义页面
<p><input type="checkbox" name="remember-me">记住我</p>
<!-- 参数:remember-me:on -->
  1. 测试
    配置session过期时间1分钟。
    登陆时勾选记住我选项,然后重启服务端,就可以在测试接口中免登录。
server:
  servlet:
    session:
      timeout: 1

三、原理分析

在这里插入图片描述1. 求到达过滤器之后,首先判断 SecurityContextHolder 中是否有值,没值的话表示用户尚未登录,此时调用autoLogin 方法进行自动登录。
2. 当自动登录成功后返回的rememberMeAuth 不为null 时,表示自动登录成功,此时调用authenticate方法对 key 进行校验,并且将登录成功的用户信息保存到SecurityContextHolder 对象中,然后调用登录成功回调,并发布登录成功事件。需要注意的是,登录成功的回调并不包含RememberMeServices 中的 1oginSuccess 方法。
3. 如果自动登录失败,则调用 remenberMeServices.loginFail方法处理登录失败回调.onUnsuccessfulAuthentication 和onSuccessfulAuthentication 都是该过滤器中定义的空方法,并没有任何实现这就是RememberMeAuthenticationFilter 过滤器所做的事情,成功将RememberMeServices的服务集成进来。

RememberMeServices

public interface RememberMeServices {
	// 从请求中获取参数,完成自动登录
	Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
	// 自动登录失败的回调
	void loginFail(HttpServletRequest request, HttpServletResponse response);
	// 自动登录成功的回调
	void loginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication);
}

在这里插入图片描述

默认使用TokenBasedRememberMeService

在这里插入图片描述processAutoLoginCookie方法用来验证 Cookie 中的令牌信息是否合法:

  1. 首先判断 cookieTokens 长度是否为3,格式不对,则直接抛出异常
  2. 从cookieTokens 数组中提取出第1项(过期时间),判断令牌是否过期,过期则抛出异常。
  3. 根据用户名(cookieTokens[1])查询出当前用户对象
  4. 调用 makeTokenSignature 方法生成一个签名,签名的生成过程如下:将用户名、令牌过期时间、用户密码以及 key组成一个宇符串,中间用:隔开,通过MD5进行加密,并将加密结果转为一个字符串返回。
  5. 判断第4步生成的签名和通过 Cookie 传来的签名是否相等(cookieTokens[1]),相等表示令牌合法,则直接返回用户对象,否则抛出异常.

在这里插入图片描述1. onLoginSuccess回调中,首先获取用户经和密码信息,如果登录成功后用户名密码从successfulAuthentication对象中擦除,则从数据库中重新加载。
2. 计算出令牌的过期时间,令牌默认有效期是两周。
3. 根据令牌的过期时间、用户名以及用户密码,计算签名
4. 调用 setCookie 方法设置 Cookie.。参数按顺序是用户名、过期时间以及签名,在setCookie 方法中会将数组转为字符串,并进行 Base64编码后响应给前端。

总结:

在这里插入图片描述

RememberMe流程图

在这里插入图片描述

四、持久化令牌

实现一:需要手动创建数据库表结构

在这里插入图片描述

@EnableWebSecurity
public class SecurityConfig2 {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .anyRequest().authenticated();
        http.formLogin();
        // 开启记住我功能
        http.rememberMe()
                // 是否总是记住我
                .alwaysRemember(true)
                // 指定rememberme的实现
                .rememberMeServices(rememberMeServices());
        http.csrf().disable();
        return http.build();
    }

    @Bean
    public RememberMeServices rememberMeServices() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 启动时候创建表结构
        tokenRepository.setCreateTableOnStartup(true);
        return new PersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),
                myUserDetailsService, tokenRepository);
    }
}

实现二:这种方式可以自动创建数据库表结构

@EnableWebSecurity
public class SecurityConfig2 {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests()
                .anyRequest().authenticated();
        http.formLogin();
        // 开启记住我功能
        http.rememberMe()
                // 是否总是记住我
                .alwaysRemember(true)
                // 指定rememberme的实现
                .tokenRepository(persistentTokenRepository());
        http.csrf().disable();
        return http.build();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        List<Map<String, Object>> result = jdbcTemplate.queryForList(" select 1 from information_schema.tables where table_name = 'persistent_logins' ");
        // 这里一直为true在后续启动时会报错,所以我们在启动之前先查询表结构是否存在,如果存在就不创建
        repository.setCreateTableOnStartup(result.isEmpty());
        repository.setDataSource(dataSource);
        return repository;
    }
}

五、前后端分离开发记住我

cookie实现:

  1. 认证成功保存记住我cookie到客户端
  2. 只有cookie写入客户端成功才能实现自动登录功能

编写自定义的MyPersistentTokenBasedRememberMeServices

public class MyPersistentTokenBasedRememberMeServices extends PersistentTokenBasedRememberMeServices {
    @Override
    protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
        // 这里可以在LoginFilter中读取出来,保存到request中。
        String paramValue = String.valueOf(request.getAttribute(AbstractRememberMeServices.DEFAULT_PARAMETER));
        // 也可以在这里获取
//        try {
//            Map<String, String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
//            String rememberVal = String.valueOf(userInfo.get(AbstractRememberMeServices.DEFAULT_PARAMETER));
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
        if (paramValue != null) {
            if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                    || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
                return true;
            }
        }
        return false;
    }

    public MyPersistentTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService, PersistentTokenRepository tokenRepository) {
        super(key, userDetailsService, tokenRepository);
    }
}

修改SecurityConfig配置

这里只展示关键部分代码,详细参考附录一说明。

/**
 * @author Huathy
 * @date 2023-03-06 23:07
 * @description
 */
// 开启web安全
@EnableWebSecurity
@Slf4j
public class SecurityConfig {
    @Autowired
    private MyUserDetailsService myUserDetailsService;
    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }

    @Bean
    public LoginVcFilter loginVcFilter() throws Exception {
        log.info(" === loginFilter init  ===");
        LoginVcFilter loginVcFilter = new LoginVcFilter();
        // 2. 指定认证处理URL
        loginVcFilter.setFilterProcessesUrl("/dologin");
        // 设置认证成功时使用自定义的记住我功能
      	loginVcFilter.setRememberMeServices(rememberMeServices());
        return loginVcFilter;
    }

    @Bean
    protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
        log.info("   === 替换了 loginVcFilter === ");
        http.addFilterAt(loginVcFilter(), UsernamePasswordAuthenticationFilter.class);
        // 开启记住我 这里是设置携带记住我cookie的处理
        http.rememberMe().rememberMeServices(rememberMeServices());
        return http.build();
    }

    @Bean
    public RememberMeServices rememberMeServices() {
        return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(), myUserDetailsService, new InMemoryTokenRepositoryImpl());
    }
}

附:

  1. 本文所涉及源码地址:https://gitee.com/huathy/study-all

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403851.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java学习笔记 --- Servlet(2)

一、HttpServletRequest类 1、基本介绍 每次只要有请求进入 Tomcat 服务器&#xff0c;Tomcat 服务器就会把请求过来的 HTTP 协议信息解析好封装到 Request 对象中。 然后传递到 service 方法&#xff08;doGet 和 doPost&#xff09;中给我们使用。我们可以通过 HttpServletR…

渗透测试 | Email信息收集

0x00 免责声明 本文仅限于学习讨论与技术知识的分享&#xff0c;不得违反当地国家的法律法规。对于传播、利用文章中提供的信息而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;本文作者不为此承担任何责任&#xff0c;一旦造成后果请自行承担…

微服务保护(Sentinel)

1.雪崩微服务链路上某个服务出现了问题&#xff0c;结果导致整个微服务调用链上所有服务都出现了问题&#xff0c;这就是雪崩。2.解决雪崩问题的常见方式有四种1.超时处理&#xff1a;设定超时时间&#xff0c;请求超过一定时间没有响应就返回错误信息&#xff0c;不会无休止等…

人工智能简单应用1-OCR分栏识别:两栏识别三栏识别都可以,本地部署完美拼接

大家好&#xff0c;我是微学AI&#xff0c;今天给大家带来OCR的分栏识别。 一、文本分栏的问题 在OCR识别过程中&#xff0c;遇到文字是两个分栏的情况确实是一个比较常见的问题。通常情况下&#xff0c;OCR引擎会将文本按照从左到右&#xff0c;从上到下的顺序一行一行地识别…

软考高项——信息文档管理

信息文档管理信息文档管理文档分类文档质量等级文档管理的规则和方法信息文档管理 信息文档管理的总线索包括&#xff1a; 1&#xff09;文档分类 2&#xff09;文档质量等级 3&#xff09; 文档分类 1、开发文档 开发过程中用到的文档 &#xff08;可行性报告、任书、需求、…

Django实践-06导出excel/pdf/echarts

文章目录Django实践-06导出excel/pdf/echartsDjango实践-06导出excel/pdf/echarts导出excel安装依赖库修改views.py添加excel导出函数修改urls.py添加excel/运行测试导出pdf安装依赖库修改views.py添加pdf导出函数修改urls.py添加pdf/生成前端统计图表修改views.py添加get_teac…

Qt读xml文件

QXmlStreamReaderQXmlStreamReader类通过简单的流式API为我们提供了一种快速的读取xml文件的方式。他比Qt自己使用的SAX解析方式还要快。所谓的流式读取即将一个xml文档读取成一系列标记的流&#xff0c;类似于SAX。而QXmlStreamReader类和SAX的主要区别就是解析这些标记的方式…

Linux自动化交互命令expect测试

介绍 expect 是由Don Libes基于Tcl&#xff08;Tool Command Language &#xff09;语言开发的&#xff0c;主要应用于自动化交互式操作的场景&#xff0c;借助Expect处理交互的命令&#xff0c;可以将交互过程如&#xff1a;ssh登录&#xff0c;ftp登录等写在一个脚本上&#…

RabbitMQ系列(1)--RabbitMQ简介

1、RabbitMQ概念RabbitMQ是一个消息中间件&#xff0c;不对消息进行处理&#xff0c;只对消息做接收、存储和转发。2、RabbitMQ四大核心概念(1)生产者产生数据发送信息的程序(2)交换机交换机是RabbitMQ中一个非常重要的部件&#xff0c;接收来着生产者的消息并把消息推送到队列…

PMP项目管理项目沟通管理

目录1 项目沟通管理2 规划沟通管理3 管理沟通4 监督沟通1 项目沟通管理 项目沟通管理包括通过开发工件&#xff0c;以及执行用于有效交换信息的各种活动&#xff0c;来确保项目及其相关方的信息需求得以满足的各个过程。项目沟通管理由两个部分组成&#xff1a;第一部分是制定…

云企业网CEN介绍与实践

云企业网CEN介绍 云企业网&#xff08;Cloud Enterprise Network&#xff09;是一款能快速构建混合云和分布式业务系统的全球网络服务。 运行在云厂商的私有全球网络上实现跨地域专有网络间&#xff0c;专有网络与本地数据中心间的私网通信 提供高效、稳定的网络传输服务适用…

【网络】什么是RPC?RPC与HTTP有什么关系?

文章目录RPC是什么RPC和HTTP的关系和区别[附]关于REST论文中提到的"HTTP不是RPC"重点参考 凤凰架构-远程过程调用 既然有HTTP为什么还要有RPC&#xff1f; RPC是什么 RPC(Remote Procedure Call)&#xff1a;即远程过程调用&#xff0c;目的是为了让计算机能够跟调用…

Android中实现滑动的7种方法

Android中实现滑动的7种方法前置知识Android坐标系视图坐标系触控事件---MotionEvent获取坐标的方法实现滑动的7种方法layout方法offsetLeftAndRight()和offsetTopAndBottom()LayoutParamsscrollTo和scrollByScroller属性动画ViewDragHelper参考前置知识 Android坐标系 Andro…

【C++进阶】面向对象

程序 编写程序是为了让计算机解决现实生活中的实际问题。pascal之父、结构化程序设计先驱Niklaus Wirth提出程序 算法 数据结构。程序是完成一定功能的一些列有序指令的集合。指令 操作码 指令。将指令按一定的顺序进行整合&#xff0c;就形成了程序。 机器语言与汇编语言…

软件测试的案例分析 - 闰年5

文章目的 显示不同的博客能获得多少博客质量分 &#xff08;这是关于博客质量分的测试 https://www.csdn.net/qc) 这个博客得了 83 分。怎么才能得到更多分数&#xff1f; 正文 我们谈了不少测试的名词, 软件是人写的, 测试计划和测试用例也是人写的, 人总会犯错误。错误发生…

Shiro学习认证和授权

Shiro学习笔记 认证 思路 获取当前的Subject&#xff0c;调用SecurityUtils.getSubject()方法&#xff1b;判断当前用户是否被认证&#xff0c;即是否已经登陆&#xff0c;调用Subject的isAuthenticated()方法进行判断&#xff1b;若没有认证&#xff0c;则把用户名和密码封…

C++11线程、互斥量以及条件变量

文章目录前言1、创建第一个线程2、线程对象的生命周期、等待和分离3、线程创建的多种方式4、互斥量4.1 独占的互斥量std::mutex4.2 递归独占互斥量recursive_mutex4.3 带超时的互斥量std::timed_mutex和std::recursive_timed_mutex4.4 std::lock_guard和std::unique_lock5、cal…

CSS常用内容总结(扫盲)

文章目录前言相关概念【了解】脚本语言什么是脚本语言脚本语言有什么特点常见的脚本语言什么是动态语言&#xff0c;什么是静态语言动态语言和静态语言两者之间有何区别CSSCSS是什么CSS的特点一、CSS代码怎么写基本语法规则引入方式内部样式内联样式表外部样式代码风格二、CSS的…

JavaWeb——进程详解

目录 一、操作系统 1、定义&#xff1a; 2、操作系统的基本功能&#xff1a; 二、进程 1、定义&#xff1a; 三、进程管理 1、PCB定义 &#xff08;1&#xff09;、身份标识 &#xff08;2&#xff09;、内存指针 &#xff08;3&#xff09;、文件描述符 2、操作系统…

Hadoop入门常见面试题与集群时间同步操作

目录 一&#xff0c;常用端口号 Hadoop3.x &#xff1a; Hadoop2.x&#xff1a; 二&#xff0c;常用配置文件&#xff1a; Hadoop3.x: Hadoop2.x: 集群时间同步&#xff1a; 时间服务器配置&#xff08;必须root用户&#xff09;&#xff1a; &#xff08;1&#xff09…