目录
引言:短信验证码是通过发送验证码到手机的一种有效的验证码系统,主要用于验证用户手机的合法性及敏感操作的身份验证。在注册和修改密码时需要用到短信验证码校验手机号的功能。本文主要集成阿里云的短信验证码功能,进行功能实现。
首先,我们需要登录阿里云官网,进入控制台搜索短信验证码(sms)服务,进行开通。
然后我们需要开通ACCESS KEY,进行api调用时需要进行填写,来验证用户的身份。
然后我们需要进入访问控制,建立用户组及用户,并分配权限SMS给他:
然后,我们可以进入短信服务,遵循的推荐顺序,进行具体功能的开通:
首先,申请签名,然后是申请短信发送的模板,然后需要记住以上内容,方便在api中进行填写。
完成上述步骤后,我们就可以开始代码的填写。
首先,在springboot项目中引入依赖:
-
- <dependency>
- <groupId>com.aliyungroupId>
- <artifactId>aliyun-java-sdk-coreartifactId>
- <version>4.3.3version>
- dependency>
然后,编写业务类代码:
- @Service
- public class AuthServiceImpl extends ServiceImpl
implements AuthService { - @Override
- public boolean sendMessage(String phoneNum, Map
code) { -
- if(StringUtils.isEmpty(phoneNum)) {
- return false;
- }
-
- /**创建阿里云连接
- * @Param regionld 默认为default
- * @Param accessKeyId 你的accessKey id
- * @Param secret 你的accessKey秘钥
- */
- DefaultProfile profile = DefaultProfile.getProfile("default","accessKeyId", "secret");
- IAcsClient client = new DefaultAcsClient(profile);
-
- //发送请求
- CommonRequest request = new CommonRequest();
- //request.setProtocol(ProtocolType.HTTPS);
- request.setMethod(MethodType.POST);
- request.setDomain("dysmsapi.aliyuncs.com");
- request.setVersion("2017-05-25");
- request.setAction("SendSms");
-
- request.putQueryParameter("PhoneNumbers", phoneNum); //发送的手机号对象
- request.putQueryParameter("SignName", "阿里云短信测试"); //申请的签名名称
- request.putQueryParameter("TemplateCode", "SMS_154950909"); //申请的短信模板
- request.putQueryParameter("TemplateParam", JSONObject.toJSONString(code)); //验证码
-
- try {
- CommonResponse response = client.getCommonResponse(request);
- System.out.println(response.getData());
- return response.getHttpResponse().isSuccess();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return false;
- }
- }
上述代码根据阿里云官网的示例代码进行修改,进行手机验证码的发送。
编写controller代码:
- //利用阿里云发送短信验证码
- @GetMapping("/send/{phone}")
- public CommonResult
- String code = redisTemplate.opsForValue().get(phone);
- if(!StringUtils.isEmpty(code)){
- log.info("验证码存在,未过期");
- }
- //不存在,生成验证码
- code = UUID.randomUUID().toString().substring(0,4);
- HashMap
map = new HashMap<>(); - map.put("code",code);
- //发送验证码
- boolean isSend = authService.sendMessage(phone,map);
- if(isSend){
- //存储验证码
- redisTemplate.opsForValue().set(phone,code, 300,TimeUnit.SECONDS);
- return CommonResult.success(null);
- }else {
- //失败
- return CommonResult.fail(null);
- }
- }
上述代码中,若发送成功,使用redis存储验证码(存储时间上述代码为5分钟,具体可以自己设定),方便后续的检验。
在手机验证码发送成功后,我们还需接收验证码进行登录验证,下面我们使用shiro集成多个realm实现。分开两个接口,一个是正常的用户名密码登录,可参考springboot集成shiro实现登录验证;另一个使用手机和验证码登录,如果手机已注册,将账号信息存入jwt token中,如果没有注册,生成临时账号,将信息存入jwt token。
下面是多个realm的实现:
(1)编写LoginToken,继承原始的UsernamePasswordToken,添加需要获取的参数及认证参数:
- public class LoginToken extends UsernamePasswordToken {
-
- //手机号
- private String phone;
- //定义登陆的类型是为了在后面的校验中 去选择使用哪一个realm
- private String loginType;
-
- public LoginToken(String phone, String loginType){
- this.phone=phone;
- this.loginType=loginType;
- }
-
- //认证规则,与realm中的认证对应
- @Override
- public Object getCredentials() {
- return phone;
- }
-
- public void setPhone(String phone) {
- this.phone = phone;
- }
-
- public String getPhone() {
- return phone;
- }
-
- public void setLoginType(String loginType) {
- this.loginType = loginType;
- }
-
- public String getLoginType() {
- return loginType;
- }
- }
(2)编写多个realm的管理类,根据loginType指定使用的realm:
- public class LoginTypeModularRealmAuthenticator extends ModularRealmAuthenticator {
-
- //就是通过传入数据的类型 来选择使用哪一个Realm
- @Override
- protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
- //做Realm的一个校验
- assertRealmsConfigured();
- //获取前端传递过来的token
- LoginToken loginToken =(LoginToken)authenticationToken;
- //现在就可以获取这个登陆的类型了
- String loginType = loginToken.getLoginType(); // 登陆类型 1:User 2:Phone
- //获取所有的realms()
- Collection
realms = getRealms(); - //登陆类型对应的所有realm全部获取到
- Collection
typeRealms=new ArrayList<>(); - for (Realm realm:realms){
- //realm类型和现在登陆的类型做一个对比
- //注意loginType中的值需要和realm中的名称对应,如PhoneRealm对应的loginType为phone
- if(realm.getName().contains(loginType)){ //就能分开这两个realm
- typeRealms.add(realm);
- }
- }
-
- if(typeRealms.size()==1){
- return doSingleRealmAuthentication(typeRealms.iterator().next(), loginToken);
- }else{
- return doMultiRealmAuthentication(typeRealms, loginToken);
- }
- }
- }
(3)编写shiro配置类,将bean注入,部分代码如下:
- //@Qualifier("userRealm") UserRealm userRealm
- @Bean
- public DefaultWebSecurityManager getDefaultWebSecurityManager(Collection
realms) { - DefaultWebSecurityManager SecurityManager = new DefaultWebSecurityManager();
- SecurityManager.setRealms(realms);
- return SecurityManager;
- }
-
- //该bean为username和password验证realm
- @Bean
- public UserRealm userRealm() {
- UserRealm userRealm = new UserRealm();
- //注册MD5加密
- userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
- return userRealm;
- }
-
- //该bean为phone验证realm
- @Bean
- public PhoneRealm phoneRealm() {
- return new PhoneRealm();
- }
-
- /**
- * 系统自带的Realm管理,主要针对多realm
- */
- @Bean
- public ModularRealmAuthenticator modularRealmAuthenticator() {
- //自己重写的ModularRealmAuthenticator
- LoginTypeModularRealmAuthenticator modularRealmAuthenticator = new LoginTypeModularRealmAuthenticator();
- modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
- return modularRealmAuthenticator;
- }
(4)编写PhoneRealm:
- @Slf4j
- public class PhoneRealm extends AuthorizingRealm {
-
- //授权
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
- return null;
- }
-
- //认证
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
-
- LoginToken token = (LoginToken) authenticationToken;
- //用户名/密码认证
- String phone = token.getPhone();
- //参数2为token中定义的getCredentials()
- return new SimpleAuthenticationInfo(phone,phone,getName());
- }
- }
上述代码只是一个手机登录的简单实现,如果需要,可在 doGetAuthenticationInfo 方法中进行扩展。
完成上述代码后,我们即可对登录的手机验证码进行验证登录。
首先,封装一个手机登录的dto,分别对于username登录和phone登录:
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public class LoginPhone implements Serializable {
-
- private static final long serialVersionUID = -5331320733431220933L;
-
- @NotBlank(message = "手机号不能为空") // 非空,message为错误的提示信息
- private String phone;
- @NotBlank(message = "验证码不能为空") // 非空
- @CodePatten // 密码自定义校验,密码必须含数字、大写字母、小写字母、特殊字符
- private String code;
- @NotBlank(message = "登录类型不能为空") // 非空,message为错误的提示信息
- private String loginType;
- }
上述代码使用了自定义注解@CodePatten进行参数的简单校验,如果不熟悉,可以删去,不影响使用;下面是自定义校验的代码:
- @Documented
- @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
- @Retention(RUNTIME)
- @Repeatable(CodePatten.List.class)
- @Constraint(validatedBy = {CodePattenValidator.class})
- public @interface CodePatten {
-
- //校验失败返回信息
- String message() default "验证码长度错误";
-
- Class>[] groups() default { };
-
- Class extends Payload>[] payload() default { };
-
- @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
- @Retention(RUNTIME)
- @Documented
- public @interface List {
- CodePatten[] value();
- }
- }
- import com.seven.springcloud.annotation.CodePatten;
- import org.apache.commons.lang3.StringUtils;
- import javax.validation.ConstraintValidator;
- import javax.validation.ConstraintValidatorContext;
-
- public class CodePattenValidator implements ConstraintValidator
{ - @Override
- public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
- if (StringUtils.isBlank(value)) {
- return false;
- }
- return validateCode(value);
- }
-
- private boolean validateCode(String code) {
- return code.length() == 4;
- }
- }
然后在控制类中编写验证短信的代码:
- @PostMapping("/login/phone")
- public CommonResult
-
- String phone=user.getPhone();
- String code=user.getCode();
- String loginType=user.getLoginType();
-
- if(!"phone".equals(loginType)){
- return new CommonResult<>(NormalResultEnum.SYSTEM_FAIL.getCode(), NormalResultEnum.SYSTEM_FAIL.getMessage());
- }
-
- if(!code.equals(redisTemplate.opsForValue().get(phone))){
- return new CommonResult<>(NormalResultEnum.FAIL.getCode(), "验证码错误");
- }
-
- //shiro验证
- Subject subject= SecurityUtils.getSubject();
- LoginToken token = new LoginToken(phone,loginType);
-
- try {
- subject.login(token);
- } catch (AuthenticationException e) {
- log.warn("用户登录异常:" + e.getMessage());
- return CommonResult.system_fail(null);
- }
-
- Map
tokenMap = new HashMap<>(); - UserAccount account = authService.getOne(new QueryWrapper
().eq("mobile",phone)); - if(account!=null){
- // 手机号已注册
- tokenMap = jwtTokenUtil
- .generateTokenAndRefreshToken(String.valueOf(account.getId()), account.getUsername(),
- //用户角色映射表中中查询用户角色
- rolesService.getOne(new QueryWrapper
() - .eq("username",account.getUsername())).getRoles());
- }else{
- // 手机号未注册,赋予user权限
- tokenMap = jwtTokenUtil
- .generateTokenAndRefreshToken(UUID.randomUUID().toString().substring(0,8), phone,"user");
- }
- return CommonResult.success(tokenMap);
- }
上述代码逻辑如下:首先判断登录类型是否为手机登录,否则不可调用该接口;然后根据手机号到redis中取对应的验证码,如果取不到或是不相同,则验证失败;然后使用shiro进行手机号的登录验证;登录成功后,将登录账号的信息存入token中,返回前端。
(此处的用户权限管理使用RBAC模型实现,创建了单独的用户权限表,若不熟悉,可以将用户权限和用户账号存入同一张表,直接取出即可)
至此,手机验证码代码实现结束,进行验证:
发送验证码:
使用手机号登录:
登录成功,返回jwt token;
查看jwt token中值,可以查得,该手机号未注册,为临时账号:
验证码错误,则登录失败。
代码中,用户名登录和手机登录可封装到同一个接口中,但LoginToken需要进行一些修改:
- public LoginToken(String username, String password, String loginType){
- super(username, password);
- this.loginType=loginType;
- }
-
- //认证规则,与realm中的认证对应
- @Override
- public Object getCredentials() {
- if(phone==null){
- return getPassword();
- }
- return phone;
- }
controller中,根据业务需求,添加如下代码即可:
- //shiro验证
- Subject subject= SecurityUtils.getSubject();
- LoginToken token = new LoginToken(username,password,loginType);
同时,最好是封装好一个同时包含phoen和username等信息的dto,方便进行参数的读取。