spring cloud security oauth2 +jwt 身份认证扩展手机号码+验证码认证

在程序的认证过程中,除了常规的用户名和密码方式,也经常会出现手机号码+密码的方式;以下,以电话号码+验证码的方式讲述OAuth2认证方式的扩展。

项目结构

手机号码登陆最重要的几个类
在这里插入图片描述

代码部分

TokenGranter接口定义token获取方法:

package com.hngtsd.zxtk.oauth.config.sms;

import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.TokenRequest;

/**
 * 默认的只有授权码类型,简化类型,密码类型,客户端类型。
 * 这里需要新增一种电话号码+验证码的认证和生成访问授权码的TokenGranter。
 * 接口TokenGranter定义了token获取方法
 */

public interface TokenGranter {
    OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}

CompositeTokenGranter实现TokenGranter:

package com.hngtsd.zxtk.oauth.config.sms;

import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.TokenRequest;

import java.util.ArrayList;
import java.util.List;

public class CompositeTokenGranter implements TokenGranter {

    // TokenGranter 列表
    private final List<TokenGranter> tokenGranters;

    public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
        this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
    }

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }

    public void addTokenGranter(TokenGranter tokenGranter) {
        if (tokenGranter == null) {
            throw new IllegalArgumentException("Token granter is null");
        }
        tokenGranters.add(tokenGranter);
    }

}

通过AuthenticationProvider接口的扩展来实现自定义认证方式:

package com.hngtsd.zxtk.oauth.config.sms;

import com.alibaba.fastjson.JSON;
import com.hngtsd.zxtk.common.utlis.CommonVo;
import com.hngtsd.zxtk.common.utlis.SmsCode;
import com.hngtsd.zxtk.oauth.service.MyUserDetailsService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 在AuthenticationManager认证过程中,
 * 是通过AuthenticationProvider接口的扩展来实现自定义认证方式的。
 * 定义手机和验证码认证提供者PhoneAndVerificationCodeAuthenticationProvider
 */
public class PhoneAndVerificationCodeAuthenticationProvider  implements AuthenticationProvider {


    /**
     * UserDetailsService
     */
    private MyUserDetailsService UserDetailsService;

    /**
     * redis服务
     */

    private StringRedisTemplate stringRedisTemplate;


    public PhoneAndVerificationCodeAuthenticationProvider(MyUserDetailsService UserDetailsService,StringRedisTemplate redisTemplate) {
        this.UserDetailsService = UserDetailsService;
        this.stringRedisTemplate = redisTemplate;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        PhoneAndVerificationCodeAuthenticationToken phoneAndVerificationCodeAuthenticationToken = (PhoneAndVerificationCodeAuthenticationToken) authentication;
        // TODO 验证授权码
        Object verificationCodeObj;
        String verificationCode = Objects.nonNull(verificationCodeObj = phoneAndVerificationCodeAuthenticationToken.getCredentials()) ?
                verificationCodeObj.toString() : StringUtils.EMPTY;

        Object phoneNumberObj;
        String phoneNumber = Objects.nonNull(phoneNumberObj = phoneAndVerificationCodeAuthenticationToken.getPrincipal())
                ? phoneNumberObj.toString() : StringUtils.EMPTY;
        // 验证用户
        if (StringUtils.isBlank(phoneNumber)) {
            throw new InternalAuthenticationServiceException("phone number is empty!");
        }
        // 根据电话号码获取用户
        UserDetails userDetails = UserDetailsService.loadUserByUsername(phoneNumber);
        if (Objects.isNull(userDetails)) {
            throw new InternalAuthenticationServiceException(
                    "UserDetailsService returned null");
        }
 /*       //校验验证码
        //验证的过期时间
        Long expire = stringRedisTemplate.getExpire( CommonVo.SMS_CODE + phoneNumber, TimeUnit.SECONDS);
        if (  expire <= 0 ) {
            throw new InternalAuthenticationServiceException(
                    "The captcha has expired!");
        }

        SmsCode cmsCode = this.getCmsCode(phoneNumber);
        if (Objects.isNull(cmsCode) && cmsCode == null) {
            throw new InternalAuthenticationServiceException(
                    "The captcha is null");
        }
        //校验验证码
        if (verificationCode == cmsCode.getCode()){
            throw new InternalAuthenticationServiceException(
                    "The captcha is Bad!");
        }
*/

        // 封装需要认证的PhoneAndVerificationCodeAuthenticationToken对象
        return new PhoneAndVerificationCodeAuthenticationToken(userDetails.getAuthorities(), phoneNumber, verificationCode);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return PhoneAndVerificationCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
    //从redis查询验证码
    public SmsCode getCmsCode(String phoneNumber) {
        //从redis中取到令牌信息
        String value = stringRedisTemplate.opsForValue().get(CommonVo.SMS_CODE + phoneNumber);
        //转成对象
        try {
            SmsCode smsCode = JSON.parseObject(value, SmsCode.class);
            return smsCode;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

在OAuth2认证开始认证时,会提前Authentication认证信息,然后交由AuthenticationManager认证

package com.hngtsd.zxtk.oauth.config.sms;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import javax.servlet.http.HttpSession;
import java.util.Collection;

/**
 * 在OAuth2认证开始认证时,
 * 会提前Authentication认证信息,
 * 然后交由AuthenticationManager认证。
 * 定义电话号码+验证码的Authentication认证信息
 */
public class PhoneAndVerificationCodeAuthenticationToken extends AbstractAuthenticationToken{

    /**
     * 手机号
     */
    private final Object phoneNumber;

    /**
     * 验证码
     */
    private final Object verificationCode;



    public PhoneAndVerificationCodeAuthenticationToken(Object phoneNumber, Object verificationCode) {
        super(null);
        this.phoneNumber = phoneNumber;
        this.verificationCode = verificationCode;
    }

    public PhoneAndVerificationCodeAuthenticationToken(Collection<? extends GrantedAuthority> authorities, Object phoneNumber, Object verificationCode) {
        super(authorities);
        this.phoneNumber = phoneNumber;
        this.verificationCode = verificationCode;
        // 认证已经通过
        setAuthenticated(true);
    }

    /**
     * 用户身份凭证(一般是密码或者验证码)
     */
    @Override
    public Object getCredentials() {
        return verificationCode;
    }

    /**
     * 身份标识(一般是姓名,手机号)
     */
    @Override
    public Object getPrincipal() {
        return phoneNumber;
    }
}

新增电话验证码类型,PhoneAndVerificationCodeTokenGranter:

package com.hngtsd.zxtk.oauth.config.sms;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 新增电话验证码类型,PhoneAndVerificationCodeTokenGranter,
 * 参考密码类型ResourceOwnerPasswordTokenGranter的认证流程,
 * 首先进行电话号码与验证码的认证,然后生成访问授权码
 */
public class PhoneAndVerificationCodeTokenGranter extends AbstractTokenGranter {

    private static final String GRANT_TYPE = "cms_code";

    private final AuthenticationManager authenticationManager;

    public PhoneAndVerificationCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
                                                ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    }

    @Autowired
    protected PhoneAndVerificationCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
                                                   ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
        this.authenticationManager = authenticationManager;
    }

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        // 电话号码与验证码
        String phoneNumber = parameters.get("mobile");
        String verificationCode = parameters.get("cms_code");

        Authentication userAuth = new PhoneAndVerificationCodeAuthenticationToken(phoneNumber, verificationCode);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        try {
            // authenticationManager进行验证
            userAuth = authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException ase) {
            //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
            throw new InvalidGrantException(ase.getMessage());
        } catch (BadCredentialsException e) {
            // If the username/password are wrong the spec says we should send 400/invalid grant
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate phone number: " + phoneNumber);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

在认证授权类上添加上手机号登陆的配置


    /**
     * 初始化所有的TokenGranter
     */
    private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {

        ClientDetailsService clientDetails = endpoints.getClientDetailsService();
        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();

        List<TokenGranter> tokenGranters = new ArrayList<>();
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
                requestFactory));
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
        tokenGranters.add(implicit);
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
        if (authenticationManager != null) {
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
                    clientDetails, requestFactory));
        }
        return tokenGranters;
    }

    /**
     *   配置令牌访问端点
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                /*  . tokenStore(tokenStore)
                       .userDetailsService(userDetailsService)*/
                .authenticationManager(authenticationManager)           //密码模式需要
                .authorizationCodeServices(authorizationCodeService)    //授权码服务
                .tokenServices(tokenServices())                         //令牌服务
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);
        // 初始化所有的TokenGranter,并且类型为CompositeTokenGranter
        List<TokenGranter> defaultTokenGranters = this.getDefaultTokenGranters(endpoints);
        defaultTokenGranters.add(new PhoneAndVerificationCodeTokenGranter(authenticationManager, endpoints.getTokenServices(),
                endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory()));

        endpoints.tokenGranter(new CompositeTokenGranter(defaultTokenGranters));
    }

WebSecurityConfig前端安全配置类上配置手机号码和验证码认证:

  /**
     * 安全拦截机制
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

     // 配置手机号码和验证码认证
        http.authenticationProvider(new PhoneAndVerificationCodeAuthenticationProvider(userDetailsService,redisTemplate))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin();

}

写一个测试发送验证码方法 我这里是将验证码放在redis中:

package com.hngtsd.zxtk.oauth.controller;


import com.alibaba.fastjson.JSON;
import com.hngtsd.zxtk.common.exception.ExceptionCast;
import com.hngtsd.zxtk.common.model.TbUser;
import com.hngtsd.zxtk.common.response.AuthCode;
import com.hngtsd.zxtk.common.response.CommonCode;
import com.hngtsd.zxtk.common.response.LoginSmsCode;
import com.hngtsd.zxtk.common.response.ResponseResult;
import com.hngtsd.zxtk.common.utlis.CommonVo;
import com.hngtsd.zxtk.common.utlis.SmsCode;
import com.hngtsd.zxtk.oauth.mapper.TbUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpSession;
import javax.xml.ws.soap.Addressing;
import java.util.concurrent.TimeUnit;

/*
 * @author Mr.wangfeng
 * @date 2020/8/5 16:51
 * @param  模拟发送短信验证码v才
 * @return
 */
@Slf4j
@RestController
public class SmsCodeController {

    @Autowired
    TbUserMapper userMapper;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    int CmsCodeValiditySeconds = 120;

    /**
     * 模拟发送短信
     *
     * @param mobile
     * @param
     */
    @GetMapping("/smscode")
    public ResponseResult SmsCode(@RequestParam String mobile) {
        TbUser tbUser = userMapper.selectByUsername(mobile);
        if (tbUser == null) {
            return new ResponseResult(AuthCode.AUTH_ACCOUNT_NOTEXISTS);
        }
        //生产随机的四位数字,过期时间为60s
        SmsCode smsCode = new SmsCode(RandomStringUtils.randomNumeric(4), 60, mobile);

        //TODO   调用短信服务提供商的接口发送短信
        log.info(smsCode.getCode() + "-----------> " + mobile);
        String codeJson = JSON.toJSONString(smsCode);
        //将验证码放入redis中
        boolean result = this.saveCmsCode(mobile,codeJson,CmsCodeValiditySeconds);
        if (!result) {
            ExceptionCast.cast(LoginSmsCode.AUTH_LOGIN_CMS_CODE_SAVEFAIL);
        }
        // session.setAttribute("sms_key", smsCode);
        return new ResponseResult(LoginSmsCode.SMS_SUCCESS);
    }
    


    /**
     * @param mobile        手机号码
     * @param content      内容
     * @param ttl          过期时间
     * @return
     */
    private boolean saveCmsCode(String mobile, String content, long ttl) {
        String key = CommonVo.SMS_CODE + mobile;
        stringRedisTemplate.boundValueOps(key).set(content, ttl, TimeUnit.SECONDS);
        Long expire = stringRedisTemplate.getExpire(key, TimeUnit.SECONDS);
        return expire > 0;
    }
}

测试

一切准备就绪后我们就可以进行测试了,先调接口发送验证码:
上面校验验证码的步骤我已经注释了,随便输入验证码都可以成功拿到令牌。
postman发送验证码
在这里插入图片描述
检查redis有没有保存成功
在这里插入图片描述

验证码拿到后我就可以测试啦!
在这里插入图片描述
数据库表我已更新到码云上了
项目地址:https://gitee.com/wf109809/security-oauth2-jwt.git

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值