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)发送短信
也算是成功了吧