如何自定义一个全新的认证方式。首先你需要知道整个流程是什么样的。
1. 实现 SmsAuthenticationToken
package com.zzh.springsecurityjson.security.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* 自定义短信验证码 AuthenticationToken
* 为什么不将 验证码也构造进来?验证码可以在 filter中进行验证
* @date 2022/2/1
* @since 1.0
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
/**
* 在未认证前,存放的是电话号码
* 认证成功后: UserDetails 对象
*/
private final Object principal;
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
this.setAuthenticated(false);
}
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
}
2. 实现 SmsAuthenticationProvider
package com.example.security_demo.sms;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
/**
* 自定义 认证 Provider
*
* @author zzh
* @date 2022/4/26
* @since 1.0
*/
public class SmsAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SmsAuthenticationToken.class, authentication,
"Only SmsAuthenticationToken is supported");
SmsAuthenticationToken token = ((SmsAuthenticationToken) authentication);
String smsCode = token.getSmsCode();
String mobile = ((String) token.getPrincipal());
// 在这里对验证码以及手机号进行验证
if (!checkCode(smsCode)) {
throw new UsernameNotFoundException("验证码错误");
}
// 如果验证码正确便可以通过电话号码到数据库或缓存中获取用户的信息,在这里需要引入一个新的概念
// UserDetails 和 权限的概念
System.out.println(mobile);
return null;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 我在这里做了简化的处理,正常情况下应该是在session 或者 redis 等缓存中获取比对
* 同时一定要注意: 如果是通过session,一定要把验证流程放到过滤器中,因为在provider对象中
* 注入 request对象会比较难以处理,可以放到 SmsAuthenticationFilter中验证也可以重新定义一个
* filter.
* @param smsCode 短信验证码
* @return 如果验证码正确返回true 反之返回false
*/
private boolean checkCode(String smsCode) {
return smsCode != null && smsCode.length() > 0;
}
}
3. 实现SmsAuthenticationFilter
package com.example.security_demo.sms;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义 手机验证码认证过滤器
*
* @author zzh
* @date 2022/4/23
* @since 1.0
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
private final static String SMS_MOBILE_KEY = "mobile";
private final static String SMS_CODE_KEY = "smsCode";
private final static AntPathRequestMatcher ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login");
private final static String POST_REQUEST_METHOD = "POST";
public SmsAuthenticationFilter() {
super(ANT_PATH_REQUEST_MATCHER);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equalsIgnoreCase(POST_REQUEST_METHOD)) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
// 从请求体中获取 电话号码和验证码 request
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
try {
JsonNode jsonBody = objectMapper.readTree(request.getInputStream());
String mobile = jsonBody.get(SMS_MOBILE_KEY).textValue();
String smsCode = jsonBody.get(SMS_CODE_KEY).textValue();
// 调用 AuthenticationManager 进行认证,
// 所以需要注入 AuthenticationManager 对象 通过父类的setAuthenticationManager 进行注入
return this.getAuthenticationManager().authenticate(new SmsAuthenticationToken( mobile, smsCode));
} catch (IOException e) {
e.printStackTrace();
}
}
// 此处调用其他逻辑 不如调用form表单登录即前后端不分离的逻辑(参考UsernamePasswordAuthenticaitonFilter),此处做简化
return null;
}
}
4. 在 SpringSecurity 配置文件中进行配置
此处一定要注意将系统的AuthenticationManager以Bean的方式进行暴露,这样会很方便我们进行认证。同时我们可以在任何地方进行注入,而不是局限于于在filter中进行验证。不如我们完全可以在controller中调用AuthenticationManager的认证方法进行认证。
package com.example.security_demo.config;
import com.example.security_demo.sms.SmsAuthenticationFilter;
import com.example.security_demo.sms.SmsAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* spring security configuration
* 如何禁用
* 需要理解 spring security 配置文件理解
* 默认的配置是什么
* @author zzh
* @date 2022/4/15
* @since 1.0
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/sms/login").permitAll().anyRequest().authenticated()
.and().csrf().disable();
// 配置 过滤器
http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 配置 AuthenticationProvider, 将自定义AuthenticationProvider注入到系统AuthenticationManager中
http.authenticationProvider(new SmsAuthenticationProvider());
}
/**
* 暴露 spring security中的 AuthenticationManager 对象,使得的用户可以在需要时进行对象注入
* 例如:
*
* 通过这种方法进行注入,然后调用
* @autowired
* AuthenticationManager authenticationManager;
*
* @return AuthenticationManager
* @throws Exception 异常
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置 filer, 此处要注意: 一定要注入AuthenticationManager
* @return
* @throws Exception
*/
@Bean
public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
return smsAuthenticationFilter;
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return (request, response, exception) -> {
response.setContentType("application/json;utf-8");
response.getWriter().write("Exception Handler");
};
}
}