oauth2.0实现短信验证码登录(无需密码)

2024-03-28 01:50

本文主要是介绍oauth2.0实现短信验证码登录(无需密码),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

为了能够快速解决这个问题,我会先说明操作步骤,步骤讲完了之后再下一篇中分析源码,说明理由.
当然,在实现短信验证码的前提是:您已经将密码模式已经整合到项目中.

好的,开整!

1.找到org.springframework.security.authentication.dao.DaoAuthenticationProvider类,这个类其实就是校验密码的类
在这里插入图片描述

2.复制全路径,将其放在自己的模块中(注意,需要和源码的存放路径保持一致)
在这里插入图片描述
3.将该类源码全部复制到自己类中,其目的是为了覆盖源码类,当oauth2.0在加载这个类的时候,走的是我们自己创建的这个类,而不会走源码类.(源码内容如下,)

/** Copyright 2004, 2005, 2006 Acegi Technology Pty Limited** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      https://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/package org.springframework.security.authentication.dao;import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.util.Assert;/*** An {@link AuthenticationProvider} implementation that retrieves user details from a* {@link UserDetailsService}.** @author Ben Alex* @author Rob Winch*/
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {// ~ Static fields/initializers// =====================================================================================/*** The plaintext password used to perform* PasswordEncoder#matches(CharSequence, String)}  on when the user is* not found to avoid SEC-2056.*/private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";// ~ Instance fields// ================================================================================================private PasswordEncoder passwordEncoder;/*** The password used to perform* {@link PasswordEncoder#matches(CharSequence, String)} on when the user is* not found to avoid SEC-2056. This is necessary, because some* {@link PasswordEncoder} implementations will short circuit if the password is not* in a valid format.*/private volatile String userNotFoundEncodedPassword;private UserDetailsService userDetailsService;private UserDetailsPasswordService userDetailsPasswordService;public DaoAuthenticationProvider() {setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());}// ~ Methods// ========================================================================================================@SuppressWarnings("deprecation")protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {if (authentication.getCredentials() == null) {logger.debug("Authentication failed: no credentials provided");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}String presentedPassword = authentication.getCredentials().toString();if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}}protected void doAfterPropertiesSet() throws Exception {Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");}protected final UserDetails retrieveUser(String username,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {prepareTimingAttackProtection();try {UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);if (loadedUser == null) {throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");}return loadedUser;}catch (UsernameNotFoundException ex) {mitigateAgainstTimingAttack(authentication);throw ex;}catch (InternalAuthenticationServiceException ex) {throw ex;}catch (Exception ex) {throw new InternalAuthenticationServiceException(ex.getMessage(), ex);}}@Overrideprotected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {boolean upgradeEncoding = this.userDetailsPasswordService != null&& this.passwordEncoder.upgradeEncoding(user.getPassword());if (upgradeEncoding) {String presentedPassword = authentication.getCredentials().toString();String newPassword = this.passwordEncoder.encode(presentedPassword);user = this.userDetailsPasswordService.updatePassword(user, newPassword);}return super.createSuccessAuthentication(principal, authentication, user);}private void prepareTimingAttackProtection() {if (this.userNotFoundEncodedPassword == null) {this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);}}private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {if (authentication.getCredentials() != null) {String presentedPassword = authentication.getCredentials().toString();this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);}}/*** Sets the PasswordEncoder instance to be used to encode and validate passwords. If* not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}** @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}* types.*/public void setPasswordEncoder(PasswordEncoder passwordEncoder) {Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");this.passwordEncoder = passwordEncoder;this.userNotFoundEncodedPassword = null;}protected PasswordEncoder getPasswordEncoder() {return passwordEncoder;}public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}protected UserDetailsService getUserDetailsService() {return userDetailsService;}public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {this.userDetailsPasswordService = userDetailsPasswordService;}
}

在该类中,我们可以看到有一段代码是在校验密码是否正确
在这里插入图片描述
当初在BEBUG的时候,我发现这个方法会走两次,第一次是走的不是用户输入的密码校验,而是客户端id和客户端密码,在postman中截图如下
在这里插入图片描述
而第二次走这个方法的时候,这个presentedPassword就是用户输入的密码了,在postman中截图如下:
在这里插入图片描述
那么我们知道,如果使用短信验证码验证的时候,是不需要进行密码校验的,第一反应当然是把密码校验的这一步给删了!!
显然不行,因为如果删了,第一步的客户端密码校验就会通不过,所以,我们可以采用双重判断,在if语句中额外增加一个条件

String presentedPassword = authentication.getCredentials().toString();if ((!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) && !presentedPassword.equals("123456")) {logger.debug("Authentication failed: password does not match stored value");throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));}

在原有的判断基础上,额外增加一个与判断,大概意思是:如果用户输入的密码和真实密码不符 并且 用户输入的密码不是123456,则报错.
那是不是能够说明,无论用户的真实密码是多少,只要用户输入的密码是123456,密码验证这一步就通过了呢?

到这里,我们的源码改造就完毕了,接下来就是业务层的逻辑处理了.

新建一个手机号登录接口,该接口参数对象内部属性是手机号和验证码

@PostMapping("/login4Phone")
@ResponseBody
public Result<Object> login4Phone(@RequestBody RequestMsg requestMsg, HttpServletResponse response) {//校验参数if (StringUtils.isEmpty(requestMsg.getPhone())) {return new Result<>(StatusCode.SUCCESS, "请输入手机号", 0);}if (StringUtils.isEmpty(requestMsg.getCode()) || Boolean.FALSE.equals(redisTemplate.hasKey("login_" + requestMsg.getPhone())) || !Objects.equals(redisTemplate.boundValueOps("login_" + requestMsg.getPhone()).get(), requestMsg.getCode())) {return new Result<>(StatusCode.SUCCESS, "验证码错误", -1);}//申请令牌 authtokenAuthToken authToken;try {authToken = authService.login4Phone(requestMsg.getPhone(), clientId, clientSecret);} catch (Exception e) {e.printStackTrace();return new Result<>(StatusCode.SUCCESS, "系统错误", -2);}//返回结果return new Result<>(StatusCode.SUCCESS, "登录成功", authToken.getJti());}

发送验证码的接口我就不放了,只需要将验证码放入Redis中即可,这部分主要在判断redis中的验证码是否和用户输入的验证码是否一致.

如果验证码输入正确了,那么我们就来到service层去申请令牌,模拟/oauth/token接口,我们知道,申请令牌在postman中是这样的

在这里插入图片描述
所以,我们只需要模拟调用这个接口即可

@Overridepublic AuthToken login4Phone(String phone, String clientId, String clientSecret) {//1.申请令牌ServiceInstance serviceInstance = loadBalancerClient.choose("oauth");URI uri = serviceInstance.getUri();String url=uri+"/oauth/token";MultiValueMap<String, String> body = new LinkedMultiValueMap<>();body.add("grant_type","password");body.add("username",phone);body.add("password","123456");MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();headers.add("Authorization",this.getHttpBasic(clientId,clientSecret));HttpEntity<MultiValueMap<String,String>> requestEntity = new HttpEntity<>(body,headers);restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){@Overridepublic void handleError(ClientHttpResponse response) throws IOException {if (response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){super.handleError(response);}}});ResponseEntity<Map> responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, Map.class);Map map = responseEntity.getBody();if (map == null || map.get("access_token") == null || map.get("refresh_token") == null || map.get("jti") == null){//申请令牌失败throw new RuntimeException("申请令牌失败");}//2.封装结果数据AuthToken authToken = new AuthToken();authToken.setAccessToken((String) map.get("access_token"));authToken.setRefreshToken((String) map.get("refresh_token"));authToken.setJti((String)map.get("jti"));//3.将jti作为redis中的key,将jwt作为redis中的value进行数据的存放stringRedisTemplate.boundValueOps(authToken.getJti()).set(authToken.getAccessToken(),ttl, TimeUnit.DAYS);return authToken;}private String getHttpBasic(String clientId, String clientSecret) {String value = clientId+":"+clientSecret;byte[] encode = Base64Utils.encode(value.getBytes());return "Basic "+new String(encode);}

这部分代码就是在模拟调用/oauth/token接口,可以看到,在body中,虽然password输入的123456,但是用户在接口中输入的是手机号和验证码,123456是我们在内部代码中加入的,所以完全可以放心用户如果输入123456的密码的问题.并且,手机登陆和密码登录时不同的两个接口,密码登录时不会走我们这个service类,唯一需要注意的是,建议把123456改成其他密码,因为这个无法阻止某些用户直接通过调用/oauth/token去申请令牌,所以密码不能过于暴露,天知地知你知我不知即可.

在此告一段落,评价一下这种方法,这种方式唯一的缺点就是略微入侵了源码,后期或许会出现一些未知问题,但好处是和密码登录方式完全一样,安全系数和oauth2.0原生安全系数一致,大大提高了安全性.

下一篇我会讲解从/oauth/token入口开始,oauth2.0是如何一步一步走向获取JWT令牌的.

这篇关于oauth2.0实现短信验证码登录(无需密码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/854093

相关文章

SpringBoot集成redisson实现延时队列教程

《SpringBoot集成redisson实现延时队列教程》文章介绍了使用Redisson实现延迟队列的完整步骤,包括依赖导入、Redis配置、工具类封装、业务枚举定义、执行器实现、Bean创建、消费... 目录1、先给项目导入Redisson依赖2、配置redis3、创建 RedissonConfig 配

Python的Darts库实现时间序列预测

《Python的Darts库实现时间序列预测》Darts一个集统计、机器学习与深度学习模型于一体的Python时间序列预测库,本文主要介绍了Python的Darts库实现时间序列预测,感兴趣的可以了解... 目录目录一、什么是 Darts?二、安装与基本配置安装 Darts导入基础模块三、时间序列数据结构与

Python使用FastAPI实现大文件分片上传与断点续传功能

《Python使用FastAPI实现大文件分片上传与断点续传功能》大文件直传常遇到超时、网络抖动失败、失败后只能重传的问题,分片上传+断点续传可以把大文件拆成若干小块逐个上传,并在中断后从已完成分片继... 目录一、接口设计二、服务端实现(FastAPI)2.1 运行环境2.2 目录结构建议2.3 serv

C#实现千万数据秒级导入的代码

《C#实现千万数据秒级导入的代码》在实际开发中excel导入很常见,现代社会中很容易遇到大数据处理业务,所以本文我就给大家分享一下千万数据秒级导入怎么实现,文中有详细的代码示例供大家参考,需要的朋友可... 目录前言一、数据存储二、处理逻辑优化前代码处理逻辑优化后的代码总结前言在实际开发中excel导入很

SpringBoot+RustFS 实现文件切片极速上传的实例代码

《SpringBoot+RustFS实现文件切片极速上传的实例代码》本文介绍利用SpringBoot和RustFS构建高性能文件切片上传系统,实现大文件秒传、断点续传和分片上传等功能,具有一定的参考... 目录一、为什么选择 RustFS + SpringBoot?二、环境准备与部署2.1 安装 RustF

Nginx部署HTTP/3的实现步骤

《Nginx部署HTTP/3的实现步骤》本文介绍了在Nginx中部署HTTP/3的详细步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录前提条件第一步:安装必要的依赖库第二步:获取并构建 BoringSSL第三步:获取 Nginx

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

Python实现Excel批量样式修改器(附完整代码)

《Python实现Excel批量样式修改器(附完整代码)》这篇文章主要为大家详细介绍了如何使用Python实现一个Excel批量样式修改器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录前言功能特性核心功能界面特性系统要求安装说明使用指南基本操作流程高级功能技术实现核心技术栈关键函

Java实现字节字符转bcd编码

《Java实现字节字符转bcd编码》BCD是一种将十进制数字编码为二进制的表示方式,常用于数字显示和存储,本文将介绍如何在Java中实现字节字符转BCD码的过程,需要的小伙伴可以了解下... 目录前言BCD码是什么Java实现字节转bcd编码方法补充总结前言BCD码(Binary-Coded Decima

SpringBoot全局域名替换的实现

《SpringBoot全局域名替换的实现》本文主要介绍了SpringBoot全局域名替换的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录 项目结构⚙️ 配置文件application.yml️ 配置类AppProperties.Ja