文章目录
原理
逻辑:先将(用户名和密码)或者(手机号)组装成未认证的Token。传给AuthenticationManager
,然后从一堆的AuthenticationProvider
中挑选适合的Provider。来处理认证请求。挑选的依据:根据provider中的support的方法是否支持传递进来的Token。在认证的过程中会调用UserdetailService
来获取用户(存在数据库中的)的信息,然后与传递进来的用户信息进行比对。如果正确的话,就将Authentication
表示为已认证。
而验证短信验证码和图形验证码是否匹配都需要在传递请求之前,通过添加过滤器进行处理。这种处理方式可以重用,在多处使用。
1.实现短信登录
1.1编写短信验证码的过滤器。
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String WU_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = "mobile";
private boolean postOnly = true; //只处理Post请求。
//请求的匹配器。
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//取出手机号
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
//去除空格
mobile = mobile.trim();
//这里封装未认证的Token
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
//将请求信息也放入到Token中。
this.setDetails(request, authRequest);
/首先进入方法这里会找到我们自己写的SmsCodeAuthenticationProvider.
/最后将结果放回到这里之后,经过AbstractAuthenticationProcessingFilter,这个抽象类的doFilter,然后调用处理器。成功调用成功处理器,失败调用失败处理器。
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号的方法
* @param request
* @return
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
//将请求信息也放入到Token中。
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String mobileParameter() {
return this.mobileParameter;
}
}
1.2编写用来封装短信的Token
**封装登录信息,身份认证之前传递的是手机号,认证成功之后:放的是用户信息。
* @author zhailiang
*
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal; //存放认证信息。
//mobile:表示手机号。
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
1.3编写处理短信Token,所需要用到的Provider,可以仿照(用户名和密码)的provider的逻辑来写。
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
//身份认证的逻辑
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
//根据手机号(Principal)去查用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername((String) authentication.getPrincipal());
if (userDetails == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//将认证信息传入进去。
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
//将请求的信息传递Token中。
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
//用于选去AuthenticationProvider的主要方法。
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
1.4编写短信验证码的校验
@Data
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler failureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private AuthenticationFailureHandler authenticationFailureHandler;
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 初始化过滤器要拦截的路径:urls
* @throws ServletException
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(),",");
if (configUrls != null && configUrls.length > 0){
for (String configUrl : configUrls) {
urls.add(configUrl);
}
}
urls.add("/mobile/login");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean flag = false;
for (String url : urls) {
//如果请求的路径在此过滤器要拦截的路径里面,则进行验证
if (pathMatcher.match(url,request.getRequestURI())){
flag = true;
}
}
if (flag){
try {
validate(new ServletWebRequest(request));
} catch (ImageCodeException e) {
//验证出现异常,使用自定义的失败处理器来处理,并且直接return,不执行后面的过滤器
failureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
/**
* 短信验证码的验证逻辑
* @param request
*/
private void validate(ServletWebRequest request) {
ValidateCode codeInSession = (ValidateCode)sessionStrategy.getAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
if (codeInSession != null){
System.out.println("session中的验证码:"+codeInSession.getCode());
String codeInRequest = null;
try {
codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
System.out.println("请求中的验证码:"+codeInRequest);
} catch (ServletRequestBindingException e) {
System.out.println(e.getMessage());
}
if (codeInSession.isExpried()){
sessionStrategy.removeAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
throw new ImageCodeException("验证码已过期");
}
if (StringUtils.isBlank(codeInRequest)){
throw new ImageCodeException("验证码不能为空");
}
if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
throw new ImageCodeException("验证错误");
}
//不抛出异常,验证码正确,删除保存的验证码
sessionStrategy.removeAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
} else {
throw new ImageCodeException("请先获取验证码");
}
}
}
2.将上述的逻辑加入到一起,放在http.中去。
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private SmsLoginService smsLoginService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
//1.因为这个过滤器需要manager进行认证,所以先设置该Mannager,在Manager中查找适合的Provider
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置失败处理器,成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
//用userLoginService来读取用户信息。
smsCodeAuthenticationProvider.setUserDetailsService(smsLoginService);
//将我们自己写的Provider加入到AuthenticationManager管的这个集合里面去。
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
先将上面写的配置类,自动注入之后。然后在public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter
这个配置类的最后写上.apply(smsCodeAuthenticationSecurityConfig)将上面的配置类也加到配置中来。
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;