Spring Security系列教程15--基于散列加密方案实现自动登录

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: 前言在前面的2个章节中,一一哥 带大家实现了在Spring Security中添加图形验证码校验功能,其实Spring Security的功能不仅仅是这些,还可以实现很多别的效果,比如实现自动登录,注销登录等。有的小伙伴会问,我们为什么要实现自动登录啊?这个需求其实还是很常见的,因为对于用户来说,他可能经常需要进行登录以及退出登录,你想想,如果用户每次登录时都要输入自己的用户名和密码,是不是很烦,用户体验是不是很不好?所以为了提高项目的用户体验,我们可以在项目中添加自动登录功能,当然也要给用户提供退出登录的功能。接下来就跟着 一一哥 来学习如何实现这些功能吧!一. 自动登录简介1

前言

在前面的2个章节中,一一哥 带大家实现了在Spring Security中添加图形验证码校验功能,其实Spring Security的功能不仅仅是这些,还可以实现很多别的效果,比如实现自动登录,注销登录等

有的小伙伴会问,我们为什么要实现自动登录啊?这个需求其实还是很常见的,因为对于用户来说,他可能经常需要进行登录以及退出登录,你想想,如果用户每次登录时都要输入自己的用户名和密码,是不是很烦,用户体验是不是很不好?

所以为了提高项目的用户体验,我们可以在项目中添加自动登录功能,当然也要给用户提供退出登录的功能。接下来就跟着 一一哥 来学习如何实现这些功能吧!

一. 自动登录简介

1. 为什么要自动登录

我们在访问网站或app时,一般都会要求我们注册一个账号,包含用户名和密码信息,其中密码还会有长度及取值范围的限制。很多时候,我们在不同的网站上注册的账号,可能密码也不同,这就导致我们必须记住这些不同网站上的用户信息。那么在下次登录时,因为我们的密码太多了,很有可能会记不起这些账号密码。所以在几次尝试登录失败之后,很多人都会选择找回密码,从而再次陷入如何设置密码的循环里。

为了尽可能减少用户重新登录的频率,提高用户的使用体验,我们可以提供自动登录这样一个会给用户带来便利,同时也会给用户带来风险的体验性功能。

2. 自动登录的实现方案

了解了自动登录出现的背景及作用后,那么我们该怎么实现自动登录呢?

首先我们知道,自动登录是将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时,自动实现校验并建立登录状态的一种机制。

所以基于上面的原理,Spring Security 就为我们提供了两种比较好的实现自动登录的方案:

  • 基于散列加密算法机制:加密用户必要的登录信息,并生成令牌来实现自动登录,利用TokenBasedRememberMeServices类来实现。
  • 基于数据库等持久化数据存储机制:生成持久化令牌来实现自动登录,利用PersistentTokenBasedRememberMeServices来实现。

我上面提到的2个实现类,其实都是AbstractRememberMeServices的子类,如下图所示:

了解了这些核心API之后,我们就可以利用这两个API来实现自动登录了。

二. 基于散列加密方案实现自动登录

我先带各位利用第1种实现方案,即基于散列加密方案来实现自动登录。

首先我们还是在之前的案例基础之上进行开发,具体的项目创建过程略过,请参考之前的章节内容。

1. 配置加密令牌的key

首先我们创建一个application.yml文件,在其中添加数据库配置,以及一个用来加密令牌的key字符串,字符串的值随便自定义就行

spring:  datasource:    url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
    username: root
    password: syc
  security:    remember-me:      key: yyg

2. 配置SecurityConfig类

跟之前的案例一样,我还是要创建一个SecurityConfig类,在其中的configure(HttpSecurity http)方法中,通过JdbcTokenRepositoryImpl关联我们的数据库,并且通过rememberMe()方法开启“记住我”功能,另外还要把我们前面在配置文件中的rememberKey配置进来,作为散列加密的key

@EnableWebSecurity(debug=true)
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter {
@Value("${spring.security.remember-me.key}")
privateStringrememberKey;
@AutowiredprivateUserDetailsServiceuserDetailsService;
@Overrideprotectedvoidconfigure(HttpSecurityhttp) throwsException {
//利用JdbcTokenRepositoryImpl关联数据源JdbcTokenRepositoryImpltokenRepository=newJdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
http.authorizeRequests()
                .antMatchers("/admin/**")
                .hasRole("ADMIN")
                .antMatchers("/user/**")
                .hasRole("USER")
                .antMatchers("/app/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
//开启“记住我”功能                .rememberMe()
                .userDetailsService(userDetailsService)
//配置散列加密用的key                .key(rememberKey)
                .and()
                .csrf()
                .disable();
    }
@BeanpublicPasswordEncoderpasswordEncoder() {
//不对登录密码进行加密returnNoOpPasswordEncoder.getInstance();
    }
}

3. 添加测试接口

为了方便后续的测试,我随便编写一个测试用的web接口。

@RestController@RequestMapping("/user")
publicclassUserController {
@GetMapping("hello")
publicStringhello() {
return"hello, user";
    }
}

4. 启动项目测试

然后我们把项目启动起来进行测试,当然你别忘了编写项目入口类,这里我就不粘贴相关代码了。

我们访问一下/user/hello接口,会先重定向到/login接口,这时候会发现在默认的登录页面上多了一个“记住我”功能。

此时如果我们打开 开发者调试工具,并且勾选“记住我”,然后发起请求,这时候我们会在控制台看到remember-me的cookie信息,说明Spring Security已经自动生成了remember-me这个cookie,且表单中的remember-me参数也处于了“on”状态。

也就是说,我们利用简单的几行代码,就实现了基于散列加密方案的自动登录。

三. 散列加密方案实现原理

你可能会很好奇,散列加密方案到底是怎么实现自动登录的呢?别急,接下来 壹哥就为你分析一下散列加密的实现原理。

1. cookie的加密原理分析

我在前面给各位说过,自动登录其实就是将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时,自动实现校验并建立登录状态的一种机制。所以在自动登录后,肯定会生成代表用户的cookie信息,但是为了安全,这个cookie肯定不会明文存储,需要把这个cookie进行加密处理,当然也会解码处理。所以接下来我就给各位分析一下这个cookie的加密和解码过程。

首先 壹哥给各位解释一下所谓的散列加密算法,其实质就是把 username、expirationTime、password等字段,再加上自定义的key字段合并起来,在每个字段之间用 ":" 分隔,最后利用md5算法进行哈希运算这样就可以得到一个加密后的字符串。Spring Security把这个加密的字符串存储到cookie中,作为用户已登录的标识信息。

然后 壹哥 带你看看TokenBasedRememberMeServices源码类中的makeTokenSignature()方法,你会看到散列加密算法的具体加密实现过程,源码如下图所示:

2. cookie的解码原理分析

上面利用MD5进行了加密,用户在下次登录后,肯定需要进行信息的比对,以判断用户信息是否一致。Spring Security是先对cookie中的信息进行解码,然后与之前记录的登录信息进行比对,以此判断用户是否已登录。

Spring Security是在AbstractRememberMeServices类的decodeCookie()方法中,利用Base64对cookie进行解码,如下图所示:

对于以上2个源码方法,我们可以简化抽取出如下两行代码:

//对各字段进行散列加密hashInfo=md5Hex(username+":"+expirationTime+":"password+":"+key)
//利用base64进行解码rememberCookie=base64(username+":"+expirationrime+":"+hashInfo)

其中,expirationTime是指本次自动登录的有效期,key是自己指定的一个散列盐值,用于防止令牌被修改。利用以上两个方法就可以实现对cookie的加密了。

3. cookie生成原理总结

分析完以上源码之后,壹哥再给各位简单总结一下cookie的生成验证原理:

  • 首先利用上面的源码生成cookie,并保存在浏览器中;
  • 在浏览器关闭并重新打开之后,用户再去访问 /user/hello 接口时,此时就会携带remember-me这个cookie到服务端;
  • 服务器端拿到cookie之后,利用Base64进行解码,计算出用户名和过期时间,再根据用户名查询到用户密码;
  • 最后还要通过 MD5 散列函数计算出散列值,并将计算出的散列值和浏览器传递来的散列值进行对比,以此确认这个令牌是否有效。

4. 自动登录的源码分析

上面分析完cookie信息的加密和解码之后,接下来我再结合源码,从两个方面来介绍自动登录的实现过程,一个是 remember-me 令牌的生成的过程,另一个则是该令牌的解析过程

4.1 令牌生成的源码分析

我们要想知道源码中是如何生成remember-me自动登录令牌的,首先得知道Spring Security是如何进入到该令牌所在代码的,这个代码的执行与我们前一章节所讲的Spring Security的认证授权有关,请进入到前面查看。

AbstractAuthenticationProcessingFilter#doFilter ->

AbstractAuthenticationProcessingFilter#successfulAuthentication ->

AbstractRememberMeServices#loginSuccess ->

TokenBasedRememberMeServices#onLoginSuccess

这个令牌生成的核心处理方法定义在:TokenBasedRememberMeServices#onLoginSuccess

@OverridepublicvoidonLoginSuccess(HttpServletRequestrequest, HttpServletResponseresponse,
AuthenticationsuccessfulAuthentication) {
//从认证对象中获取用户名Stringusername=retrieveUserName(successfulAuthentication);
//从认证对象中获取密码Stringpassword=retrievePassword(successfulAuthentication);
    ......
if (!StringUtils.hasLength(password)) {
//根据用户名查询出对应的用户UserDetailsuser=getUserDetailsService().loadUserByUsername(username);
//获取到用户身上的密码password=user.getPassword();
    }
//获取登录过期时间,默认是2周inttokenLifetime=calculateLoginLifetime(request, successfulAuthentication);
longexpiryTime=System.currentTimeMillis();
expiryTime+=1000L* (tokenLifetime<0?TWO_WEEKS_S : tokenLifetime);
//生成remember-me签名信息StringsignatureValue=makeTokenSignature(expiryTime, username, password);
//保存cookiesetCookie(newString[] { username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
}
protectedStringmakeTokenSignature(longtokenExpiryTime, Stringusername,
Stringpassword) {
Stringdata=username+":"+tokenExpiryTime+":"+password+":"+getKey();
MessageDigestdigest;
digest=MessageDigest.getInstance("MD5");
returnnewString(Hex.encode(digest.digest(data.getBytes())));
}
以上源码的实现逻辑很好理解:
  1. 首先从登录成功的 Authentication 对象中提取出用户名/密码;
  2. 由于登录成功之后,密码可能被擦除了,所以如果一开始没有拿到密码,就再从 UserDetailsService 中重新加载用户并重新获取密码;
  3. 接下来获取令牌的有效期,令牌有效期默认是两周;
  4. 再接下来调用 makeTokenSignature()方法 去计算散列值,实际上就是根据 username、令牌有效期以及 password、key 一起计算一个散列值。如果我们没有自己去设置这个 key,默认是在 RememberMeConfigurer#getKey 方法中进行设置的,它的值是一个 UUID 字符串。但是如果服务端重启,这个默认的 key 是会变的,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,所以我们可以指定这个 key。
  5. 最后,将用户名、令牌有效期以及计算得到的散列值放入 Cookie 中并随response返回。


4.2 令牌解析的源码分析

对于RememberMe 这个功能,Spring Security提供了 RememberMeAuthenticationFilter 这个过滤器类来处理相关功能,我们来看下 RememberMeAuthenticationFilter 的 doFilter() 方法:

publicvoiddoFilter(ServletRequestreq, ServletResponseres, FilterChainchain)
throwsIOException, ServletException {
HttpServletRequestrequest= (HttpServletRequest) req;
HttpServletResponseresponse= (HttpServletResponse) res;
if (SecurityContextHolder.getContext().getAuthentication() ==null) {
//处理自动登录的业务逻辑AuthenticationrememberMeAuth=rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth!=null) {
// Attempt authenticaton via AuthenticationManagertry {
rememberMeAuth=authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolderSecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder populated with remember-me token: '"+SecurityContextHolder.getContext().getAuthentication()
+"'");
     }
// Fire eventif (this.eventPublisher!=null) {
eventPublisher        .publishEvent(newInteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext()
            .getAuthentication(), this.getClass()));
     }
if (successHandler!=null) {
successHandler.onAuthenticationSuccess(request, response,
rememberMeAuth);
return;
     }
    }
catch (AuthenticationExceptionauthenticationException) {
if (logger.isDebugEnabled()) {
logger.debug(
"SecurityContextHolder not populated with remember-me token, as "+"AuthenticationManager rejected Authentication returned by RememberMeServices: '"+rememberMeAuth+"'; invalidating remember-me token",
authenticationException);
     }
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response,
authenticationException);
    }
   }
chain.doFilter(request, response);
    }
else {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"+SecurityContextHolder.getContext().getAuthentication() +"'");
   }
chain.doFilter(request, response);
    }
 }

这个方法最关键的地方在于,如果从 SecurityContextHolder 中无法获取到当前登录用户实例,那么就调用 rememberMeServices.autoLogin()逻辑进行登录,我们来看下这个方法:

@OverridepublicfinalAuthenticationautoLogin(HttpServletRequestrequest,
HttpServletResponseresponse) {
StringrememberMeCookie=extractRememberMeCookie(request);
if (rememberMeCookie==null) {
returnnull;
    }
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() ==0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
returnnull;
    }
UserDetailsuser=null;
try {
String[] cookieTokens=decodeCookie(rememberMeCookie);
user=processAutoLoginCookie(cookieTokens, request, response);
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
returncreateSuccessfulAuthentication(request, user);
    }
    ......
cancelCookie(request, response);
returnnull;
 }

Spring Security就是在这里提取出 cookie 信息,并对 cookie 信息进行解码。解码之后,再调用 processAutoLoginCookie()方法去做校验。processAutoLoginCookie() 方法的代码我就不贴了,核心流程就是首先获取用户名和过期时间,再根据用户名查询到用户密码,然后通过 MD5 散列函数计算出散列值。最后再将拿到的散列值和浏览器传递来的散列值进行对比,就能确认这个令牌是否有效,进而确认登录是否有效。

至此,壹哥 就结合着源码和底层原理,给大家讲解了基于散列加密方案实现了自动登录,并且在本案例中给大家介绍了散列加密算法,你掌握的怎么样呢?请在评论区给 一一哥 留言,说说你的感受吧!下一篇文章中,壹哥 会给各位讲解 基于持久化令牌方案实现自动登录,敬请期待哦!

相关文章
|
2天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
16天前
|
存储 安全 Java
|
15天前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
本指南介绍了如何在Spring Boot项目中集成Firebase云消息服务(FCM),包括创建项目、添加依赖、配置服务账户密钥、编写推送服务类以及发送消息等步骤,帮助开发者快速实现推送通知功能。
41 2
|
2月前
|
XML JavaScript Java
Spring Retry 教程
Spring Retry 是 Spring 提供的用于处理方法重试的库,通过 AOP 提供声明式重试机制,不侵入业务逻辑代码。主要步骤包括:添加依赖、启用重试机制、设置重试策略(如异常类型、重试次数、延迟策略等),并可定义重试失败后的回调方法。适用于因瞬时故障导致的操作失败场景。
Spring Retry 教程
|
2月前
|
前端开发 Java Spring
【非降版本解决】高版本Spring boot Swagger 报错解决方案
【非降版本解决】高版本Spring boot Swagger 报错解决方案
|
1月前
|
JSON Java Maven
实现Java Spring Boot FCM推送教程
详细介绍实现Java Spring Boot FCM推送教程
82 0
|
3月前
|
Java 数据库连接 Spring
一文讲明 Spring 的使用 【全网超详细教程】
这篇文章是一份全面的Spring框架使用教程,涵盖了从基础的项目搭建、IOC和AOP概念的介绍,到Spring的依赖注入、动态代理、事务处理等高级主题,并通过代码示例和配置文件展示了如何在实际项目中应用Spring框架的各种功能。
一文讲明 Spring 的使用 【全网超详细教程】
|
2月前
|
存储 安全 数据库
双重防护,无懈可击!Python AES+RSA加密方案,构建最强数据安全堡垒
【9月更文挑战第11天】在数字时代,数据安全至关重要。AES与RSA加密技术相结合,构成了一道坚固防线。AES以其高效性保障数据加密,而RSA则确保密钥安全传输,二者相辅相成,提供双重保护。本文通过Python代码示例展示了这一加密方案的魅力,强调了其在实际应用中的重要性和安全性。使用HTTPS等安全协议传输加密密钥和密文,确保数据在数字世界中自由流通而无忧。
59 1
|
1月前
|
存储 NoSQL Java
Spring Boot项目中使用Redis实现接口幂等性的方案
通过上述方法,可以有效地在Spring Boot项目中利用Redis实现接口幂等性,既保证了接口操作的安全性,又提高了系统的可靠性。
32 0
|
3月前
|
存储 安全 数据库
双重防护,无懈可击!Python AES+RSA加密方案,构建最强数据安全堡垒
【8月更文挑战第3天】在数字时代,数据安全至关重要。Python AES+RSA加密方案提供了一种强大且可靠的数据保护方式。AES以高效安全著称,适用于大量数据的快速加密;RSA作为非对称加密技术,确保了密钥传输的安全性。二者结合形成“内外兼修”的加密策略:AES加密数据内容,RSA保护AES密钥,共同构建起数据安全的双重保险。通过示例代码展示了这一加密流程,强调了加密后密钥与密文的安全传输和存储的重要性。在实际应用中,应采用HTTPS等安全协议进行传输,并将数据安全存储于加密的数据库或文件系统中。
78 12