由 SpringSecurity(四)认证流程 我们已经知道了Spring Security
用户名和密码的登录流程。仿照用户名和密码登录编写一个短信验证码登录
手机验证码登录流程图
短信验证码
新建一个SmsCode类,里面有三个属性:String code(验证码字符串)、LocalDateTime expireTime(过期时间)、String mobile(手机号)。省略短信验证码的发送过程。大概步骤如下:
1. 用户点击发送验证码按钮
2. 随机生成一个code字符串,新建SmsCode对象,设置验证码字符串code、手机号mobile和过期时间expireTime
3. 将SmsCode对象存入session
实现手机验证码登录
1. 新建 SmsCodeAuthenticationToken,对应用户名密码登录的UsernamePasswordAuthenticationToken
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 500L;
private final Object principal;
public SmsCodeAuthenticationToken(Object mobile) {
super((Collection)null);
this.principal = mobile;
this.setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
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");
} else {
super.setAuthenticated(false);
}
}
public void eraseCredentials() {
super.eraseCredentials();
}
}
2. 新建 SmsCodeAuthenticationFilter,对应用户名密码登录的UsernamePasswordAuthenticationFilter
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
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();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return this.mobileParameter;
}
}
3. 新建 SmsCodeAuthenticationProvider,对应用户名密码登录的DaoAuthenticationProvider
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private MyUserService myUserService;
public MyUserService getMyUserService() {
return myUserService;
}
public void setMyUserService(MyUserService myUserService) {
this.myUserService = myUserService;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication;
UserDetails user = myUserService.loadUserByMobile((String)smsCodeAuthenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities());
result.setDetails(smsCodeAuthenticationToken.getDetails());
return result;
}
@Override
public boolean supports(Class<?> aClass) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
}
}
4. 修改 MyUserService
@Component
public class MyUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
LoginUser loginUser = new LoginUser(s,new BCryptPasswordEncoder().encode("123456"),authorityLists);
loginUser.setNickName("成");
return loginUser;
}
public UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException {
// 通过手机号mobile去数据库里查找用户以及用户权限
List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
LoginUser loginUser = new LoginUser(mobile,new BCryptPasswordEncoder().encode("123456"),authorityLists);
loginUser.setNickName("成");
return loginUser;
}
}
5. 新建 SmsCodeAuthenticationSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private MyAuthenticationFailHandler myAuthenticationFailHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyUserService MyUserService;
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setMyUserService(MyUserService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
6. 新建SmsCodeFilter过滤器,用于验证短信验证码是否正确。使用过滤器来验证短信验证码的好处是,可以任意设置哪些地址需要短信验证码验证之后才能访问,不仅仅只使用于登录。
@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private Set<String> urls = new HashSet<>();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
// 这里配置需要拦截的地址 ......
urls.add("/authentication/mobile"); //
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) {
action = true;
break;
}
}
if (action) {
try {
validate(httpServletRequest);
} catch (SmsCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(HttpServletRequest request) {
SmsCode smsCode = (SmsCode)request.getSession().getAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
String smsCodeRequest = request.getParameter("smsCode");
if (smsCodeRequest == null || smsCodeRequest.isEmpty()) {
throw new SmsCodeException("短信验证码不能为空");
}
if (smsCode == null) {
throw new SmsCodeException("短信验证码不存在");
}
if (smsCode.isExpired()) {
request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
throw new SmsCodeException("短信验证码已过期");
}
if(!smsCodeRequest.equalsIgnoreCase(smsCode.getCode())) {
throw new SmsCodeException("短信验证码错误");
}
if(!smsCode.getMobile().equals(request.getParameter("mobile"))) {
throw new SmsCodeException("输入的手机号与发送短信验证码的手机号不一致");
}
request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
}
}
7. SpringSecurityConfig配置文件,将SmsCodeAuthenticationSecurityConfig和SmsCodeFilter注入
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserService myUserService;
@Autowired
private MyAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private MyAuthenticationFailHandler authenticationFailHandler;
@Autowired
private ImageCodeFilter imageCodeFilter;
@Autowired
private SmsCodeFilter smsCodeFilter;
// 注入短信登录的相关配置
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) // 将ImageCodeFilter过滤器设置在UsernamePasswordAuthenticationFilter之前
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/authentication/*","/login/*","/code/*") // 不需要登录就可以访问
.permitAll()
.antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能访问
.antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能访问
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/authentication/login") // 访问需要登录才能访问的页面,如果未登录,会跳转到该地址来
.loginProcessingUrl("/authentication/form")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailHandler)
;
http.apply(smsCodeAuthenticationSecurityConfig);
}
// 密码加密方式
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 重写方法,自定义用户
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");
// auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
auth.userDetailsService(myUserService); // 注入MyUserService,这样SpringSecurity会调用里面的loadUserByUsername(String s)
}
}
代码地址 https://github.com/923226145/SpringSecurity/tree/master/chapter5