spring security 手机号 短信验证码认证 替换默认的用户名密码认证

         spring security内置的有用户名密码认证规则,还可以调用第三方微信、qq登录接口实现登录认证,这里使用自定义的手机号和短信验证码实现登录认证。   

         要实现自定义的手机号和短信验证码认证需要了解用户名密码认证的逻辑,仿照该逻辑就可以写出任何自定义的登录认证:

                Filter用来过滤出对应的登录请求

                Manager用来寻找具体能匹配上Token的Provider对象校验

                Provider调用Service,Service返回一个已经存储的用户信息封装为Token对象,

 Provider 拿去该Token对象和用户登录封装的Token值比较。如果匹配成功返回一个新的已认证的Token。

        一、熟悉security框架用户名密码校验逻辑,Filter、Provider、Authentication。  

0)UsernamePasswordAuthenticationToken

        UsernamePasswordAuthenticationToken就是Authentication的一个实现类

        



1)UsernamePasswordAuthenticationFilter:



2)AuthenticationManager

        security实现认证的实现类为ProviderManager。



3)DaoAuthenticationProvider

                ①AbstractUserDetailsAuthenticationProvider

                                       DaoAuthenticationProvider的authenticate()方法在父类                     AbstractUserDetailsAuthenticationProvider中;

             ②  DaoAuthenticationProvider的retrieveUser()方法

   



            4)UserDetilsService

                        调用实现类WebSecurityConfigurerAdapter完成创建一个包含用户名和密码的Authentication对象。

                        密码由内部类随机生成。

 二、自定义手机号验证码登录认证。

                实现该认证,只需要重写Token,Filter,Provider,UserDetilsService。这里验证码可以使用阿里云的免费短信测试,因为我的白嫖短信已经过期,所以这里事先写死验证码,最后调用阿里云的短信验证API。

                0)登录业务的两个url

                                用户点击发送短信调用的单元方法,使用直接响应

    @RequestMapping("/sendPhoneCode")
    @ResponseBody
    public String sendPhoneCode(String phone,HttpSession session) throws ExecutionException, InterruptedException {
        String code = RandomNumberCode.creatCode();
        SendSmsResponse sendSmsResponse = SmsUtils.sendCode(phone,code);
        session.setAttribute("smsCode",code);
//        if (sendSmsResponse.getStatusCode() == 200){
//            return AjaxResultVo.success(200);
//        }
        return AjaxResultVo.success(200);
    }

                              用户点击登录的拦截url,使用过滤器实现

        

           1)Token

                             模仿UsernamePasswordAuthenticationToken,把密码删除。验证码直接在controller层比较,如果验证码不对没必要比较手机号是否注册。

public class SmsAuthenticationToekn extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;


    public SmsAuthenticationToekn(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    public SmsAuthenticationToekn(Object principal,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }




    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}


        2)Filter

                修改拦截路径,比较发送短信的验证码和用户的验证码是否一致,不一致直接异常。这里发送短信的验证码直接为1234

package com.xja.filter;

import com.xja.domain.SmsAuthenticationToekn;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author rk
 * @description: TODO
 * @date 2024/9/16 19:27
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/phoneLogin",
            "POST");

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;

    private boolean postOnly = true;





    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        System.out.println("过滤器生效");
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            }


        HttpSession session = request.getSession();
        String smsCode = (String) session.getAttribute("smsCode");
//        if (smsCode.equals(request.getParameter("code"))){
//                throw  new UsernameNotFoundException("用户名或验证码错误");
//            }
        if (!"1234".equals(request.getParameter("code"))){
                throw  new UsernameNotFoundException("用户名或验证码错误");
            }

            String username = obtainUsername(request);
            username = (username != null) ? username : "";
            username = username.trim();
            SmsAuthenticationToekn authRequest = new SmsAuthenticationToekn(username);

            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(this.usernameParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsAuthenticationToekn authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}


        3)Provider

                如果比较成功 返回一个新创建的Token对象存储用户信息。

public class SmsAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        System.out.println("校验器生效");
       SmsAuthenticationToekn smsAuthenticationToekn = (SmsAuthenticationToekn) authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername(smsAuthenticationToekn.getName());
        if (userDetails == null){
            throw  new UsernameNotFoundException("用户名或验证码错误");
        }
        System.out.println();
        return new SmsAuthenticationToekn(userDetails.getUsername(),userDetails.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (SmsAuthenticationToekn.class.isAssignableFrom(authentication));
    }

}


        4)UserDetilsService

                        从数据库中校验是否有该手机号,这里的业务是没有提示用户输入有误,

很多登录界面使用手机号验证码时不需要注册,那么在这里也可以直接调用mapper注册该手机号.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        System.out.println("service生效");
        User user = userMapper.SelectUserByUserName(name);
        System.out.println("user = " + user.toString());
        if (user == null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        List<SimpleGrantedAuthority> powerNameList = new ArrayList<SimpleGrantedAuthority>();
        powerNameList.add(new SimpleGrantedAuthority("ROLE_"+"userManage"));
        powerNameList.add(new SimpleGrantedAuthority("system:user:add"));

        return new org.springframework.security.core.userdetails.User(
                user.getUname(),
                user.getUpassword(),
                powerNameList
                );
    }

        5)调用阿里云api发送手机验证码

        5.1)复制SDK,导入依赖

                     AccessKey需要手动创建

                     手机号和验证码在用户发送请求时获取

        5.2)创建AccessKey

        5.3)封装发送短信的工具类

package com.xja.util;

import com.aliyun.auth.credentials.Credential;
import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
import com.aliyun.sdk.service.dysmsapi20170525.AsyncClient;
import com.aliyun.sdk.service.dysmsapi20170525.models.AddSmsSignRequest;
import com.aliyun.sdk.service.dysmsapi20170525.models.AddSmsSignResponse;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse;
import com.google.gson.Gson;
import darabonba.core.client.ClientOverrideConfiguration;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * @author rk
 * @description: TODO
 * @date 2024/9/18 17:07
 */
public class SmsUtils {

    public static SendSmsResponse sendCode(String phone,String code) throws ExecutionException, InterruptedException {
        // HttpClient Configuration
        /*HttpClient httpClient = new ApacheAsyncHttpClientBuilder()
                .connectionTimeout(Duration.ofSeconds(10)) // Set the connection timeout time, the default is 10 seconds
                .responseTimeout(Duration.ofSeconds(10)) // Set the response timeout time, the default is 20 seconds
                .maxConnections(128) // Set the connection pool size
                .maxIdleTimeOut(Duration.ofSeconds(50)) // Set the connection pool timeout, the default is 30 seconds
                // Configure the proxy
                .proxy(new ProxyOptions(ProxyOptions.Type.HTTP, new InetSocketAddress("<your-proxy-hostname>", 9001))
                        .setCredentials("<your-proxy-username>", "<your-proxy-password>"))
                // If it is an https connection, you need to configure the certificate, or ignore the certificate(.ignoreSSL(true))
                .x509TrustManagers(new X509TrustManager[]{})
                .keyManagers(new KeyManager[]{})
                .ignoreSSL(false)
                .build();*/

        // Configure Credentials authentication information, including ak, secret, token
        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
                // Please ensure that the environment variables ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET are set.
                .accessKeyId("xxx")
                .accessKeySecret("xxx")
                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
                .build());

        // Configure the Client
        AsyncClient client = AsyncClient.builder()
                .region("cn-hangzhou") // Region ID
                //.httpClient(httpClient) // Use the configured HttpClient, otherwise use the default HttpClient (Apache HttpClient)
                .credentialsProvider(provider)
                //.serviceConfiguration(Configuration.create()) // Service-level configuration
                // Client-level configuration rewrite, can set Endpoint, Http request parameters, etc.
                .overrideConfiguration(
                        ClientOverrideConfiguration.create()
                                // Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
                                .setEndpointOverride("dysmsapi.aliyuncs.com")
                        //.setConnectTimeout(Duration.ofSeconds(30))
                )
                .build();

        // Parameter settings for API request
        SendSmsRequest sendSmsRequest = SendSmsRequest.builder()
                .signName("阿里云短信测试")
                .templateCode("SMS_134567")
                .phoneNumbers(phone)
                .templateParam("{\"code\":\""+code+"\"}")
                // Request-level configuration rewrite, can set Http request parameters, etc.
                // .requestConfiguration(RequestConfiguration.create().setHttpHeaders(new HttpHeaders()))
                .build();

        // Asynchronously get the return value of the API request
        CompletableFuture<SendSmsResponse> response = client.sendSms(sendSmsRequest);
        // Synchronously get the return value of the API request
        SendSmsResponse resp = response.get();
        System.out.println(new Gson().toJson(resp));
        // Asynchronous processing of return values
        /*response.thenAccept(resp -> {
            System.out.println(new Gson().toJson(resp));
        }).exceptionally(throwable -> { // Handling exceptions
            System.out.println(throwable.getMessage());
            return null;
        });*/

        // Finally, close the client
        client.close();
        return resp;
    }
}

 6)发送短信

        也算是成功了吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值