1 security07 子工程Security默认提供的是用户名密码登录模式,然后我们参考用户名密码登录自定义实现短信登录模式
这样就多了一种登录模式,在登录的时候可以自行选择登录模式
学习本篇之前,最好先了解下上一篇 认证流程篇
4.0.0 com.yzm security 0.0.1-SNAPSHOT ../pom.xml security07 0.0.1-SNAPSHOT jar security07 Demo project for Spring Boot com.yzm common 0.0.1-SNAPSHOT org.springframework.boot spring-boot-maven-plugin
项目结构
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test04?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
username: root
password: root
main:
allow-bean-definition-overriding: true
mybatis-plus:
mapper-locations: classpath:/mapper
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// form表单中手机号码的字段name
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
// 拦截/sms/login
private static final AntPathRequestMatcher DEFAULT_SMS_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
private String mobileParameter = "mobile";
private boolean postonly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_SMS_PATH_REQUEST_MATCHER);
}
public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_SMS_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (this.postonly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = this.obtainMobile(request);
mobile = mobile != null ? mobile.trim() : "";
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsAuthenticationToken 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 String getMobileParameter() {
return this.mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postonly = postOnly;
}
}
对应的UsernamePasswordAuthenticationToken 改造成 SmsAuthenticationToken
package com.yzm.security07.config;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 554008100412847685L;
private final Object principal;
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
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;
}
@Override
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();
}
}
3 短信认证 Provider
我这里自定义的短信认证 Provider
把AbstractUserDetailsAuthenticationProvider跟其子类DaoAuthenticationProvider的作用结合一起写了
package com.yzm.security07.config;
import com.yzm.common.utils.HttpUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
public class SmsAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public SmsAuthenticationProvider(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public boolean supports(Class> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
checkSmsCode(mobile);
// 相当于DaoAuthenticationProvider的retrieveUser()
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 相当于AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication()
SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
// 检查验证码
private void checkSmsCode(String mobile) {
HttpServletRequest request = HttpUtils.getHttpServletRequest();
String inputCode = request.getParameter("smsCode");
Map smsMap = (Map) request.getSession().getAttribute("smsCode");
if (smsMap == null) {
throw new BadCredentialsException("未检测到申请验证码");
}
String applyMobile = (String) smsMap.get("mobile");
int code = (int) smsMap.get("code");
if (!applyMobile.equals(mobile)) {
throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
}
if (code != Integer.parseInt(inputCode)) {
throw new BadCredentialsException("验证码错误");
}
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
}
4 SecurityConfig 配置类
将自定义的SmsAuthenticationFilter添加到Security框架的过滤链中
package com.yzm.security07.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
public SecurityConfig(@Qualifier("secUserDetailsServiceImpl") UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 从数据库获取用户
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//配置资源权限规则
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭CSRF跨域
.csrf().disable()
// addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter
.addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
.authenticationProvider(new SmsAuthenticationProvider(userDetailsService))
// 登录
.formLogin()
.loginPage("/auth/login") //指定登录页的路径,默认/login
.loginProcessingUrl("/login") //指定自定义form表单请求的路径(必须跟login.html中的form action=“url”一致)
.defaultSuccessUrl("/home", true) // 登录成功后的跳转url地址
.failureUrl("/auth/login?error") // 登录失败后的跳转url地址
.permitAll()
.and()
.exceptionHandling()
.accessDeniedPage("/401") // 拒接访问跳转页面
.and()
// 退出登录
.logout()
.permitAll()
.and()
// 访问路径URL的授权策略,如注册、登录免登录认证等
.authorizeRequests()
.antMatchers("/", "/home", "/register", "/auth/login").permitAll() //指定url放行
.antMatchers("/sms/code").permitAll() //获取短信验证码
.anyRequest().authenticated() //其他任何请求都需要身份认证
;
}
}
5 验证码接口
此次演示只是模拟手机短信登录,手机号既是用户名,验证码是随机数并存储到session中的
在HomeController新增接口
@GetMapping("/sms/code")
public String sms(String mobile, HttpSession session) {
int code = (int) Math.ceil(Math.random() * 9000 + 1000);
Map map = new HashMap<>(16);
map.put("mobile", mobile);
map.put("code", code);
session.setAttribute("smsCode", map);
log.info("{}:为 {} 设置短信验证码:{}", session.getId(), mobile, code);
return "redirect:/auth/login";
}
放行
.antMatchers("/sms/code").permitAll() //获取短信验证码
6 登录页面
提供两种登录模式
login.html
登录页
Invalid username or password.
用户名密码登录
短信登录
7 测试启动项目,登录页
用户名密码登录yzm,没问题
退出/logout 换短信登录,先获取验证码
输入验证码进行登录,也是没问题的
1 首先入口还是 AbstractAuthenticationProcessingFilter#doFilter
// addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter
.addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
new AntPathRequestMatcher("/sms/login", "POST");
new AntPathRequestMatcher("/login", "POST");
这里我们是把短信认证放在用户名密码认证之后,
SmsAuthenticationFilter 拦截路径是/sms/login,而UsernamePasswordAuthenticationFilter是/login
短信登录发送请求/sms/login,
不是UsernamePasswordAuthenticationFilter需要拦截的,所以放行
是被我们自定义的SmsAuthenticationFilter拦截了,所以走了SmsAuthenticationFilter过滤器
2 AbstractAuthenticationProcessingFilter调用SmsAuthenticationFilter#attemptAuthentication()方法
3 SmsAuthenticationFilter构造未认证的SmsAuthenticationToken并提交给认证管理器AuthenticationManager
4 由认证管理器的子类ProviderManager遍历查找有没有支持SmsAuthenticationToken的AuthenticationProvider,发现我们自定义的SmsAuthenticationProvider支持
5 SmsAuthenticationProvider获取用户信息,并构造完全认证的Authentication对象返回
6 AbstractAuthenticationProcessingFilter接收的返回的完全认证Authentication对象之后,进行其他处理



