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

相关文章

Security OAuth2 单点登录流程

单点登录(英语:Single sign-on,缩写为 SSO),又译为单一签入,一种对于许多相互关连,但是又是各自独立的软件系统,提供访问控制的属性。当拥有这项属性时,当用户登录时,就可以获取所有系统的访问权限,不用对每个单一系统都逐一登录。这项功能通常是以轻型目录访问协议(LDAP)来实现,在服务器上会将用户信息存储到LDAP数据库中。相同的,单一注销(single sign-off)就是指

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

【测试】输入正确用户名和密码,点击登录没有响应的可能性原因

目录 一、前端问题 1. 界面交互问题 2. 输入数据校验问题 二、网络问题 1. 网络连接中断 2. 代理设置问题 三、后端问题 1. 服务器故障 2. 数据库问题 3. 权限问题: 四、其他问题 1. 缓存问题 2. 第三方服务问题 3. 配置问题 一、前端问题 1. 界面交互问题 登录按钮的点击事件未正确绑定,导致点击后无法触发登录操作。 页面可能存在

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、