文章目录
前言
一般程序的权限认证模块在登陆功能上,一般会扩展其他登陆方式,如短信登陆,扫码登录
这里我们主要讲一下怎么扩展短信认证登录
验证码存储我们采用redis作为缓存
(注意,这里为了测试方便改为手动设置验证码)
/**
* @Auther: jiliugang
* @Date: 2020/9/21 10:32
* @Description:
*/
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("sms")
public Result sms(String code, String mobile){
//这里需要判断手机号是否注册过,此处我省略了
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("sms:" + mobile, code, 1, TimeUnit.MINUTES);
if (!aBoolean){
return ResultUtil.error(-1,"请求过于频繁");
}else {
return ResultUtil.success();
}
}
}
经过上面接口,验证码已经存入到redis中,下面开始认证流程
自定义SmsCodeAuthenticationToken短信验证码认证token
##security默认登陆方式用的类为UsernamePasswordAuthenticationToken,我们可以以此为模板进行修改,变成我们想要的
下面是SmsCodeAuthenticationToken代码
/**
* @Auther: jiliugang
* @Date: 2020/9/18 13:54
* @Description:
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 530L;
private final Object principal;
private final String code;
/**
* 认证成功前
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile,String code) {
super(null);
this.principal = mobile;
this.code = code;
super.setAuthenticated(false);
}
/**
* 认证成功后
* @param principal
* @param authorities
*/
public SmsCodeAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
code = null;
}
@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");
} else {
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
public String getCode() {
return code;
}
}
有了sms认证token后,我们还要自定义登录过滤器SmsCodeAuthenticationFilter,专门用来处理短信登录路径
依然是参照security默认登录的过滤器UsernamePasswordAuthenticationFilter来实现自己的代码
/**
* @Auther: jiliugang
* @Date: 2020/9/18 14:02
* @Description:
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParam = "mobile";
private String mobileCode = "code";
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/mobileAndCode/login", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("不支持该类型请求: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
String code = this.obtainCode(request);
if (mobile == null) {
mobile = "";
}
if (code == null) {
code = "";
}
mobile = mobile.trim();
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile,code);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
this.mobileParam = mobileParameter;
}
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParam);
}
@Nullable
protected String obtainCode(HttpServletRequest request) {
return request.getParameter(this.mobileCode);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public final String getMobileParameter() {
return mobileParam;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}
有了这两个后,我们还需要一个处理认证的provider,自定义一个SmsCodeAuthenticationProvider
依然是参照官方默认的DaoAuthenticationProvider,来实现我们自定义的
/**
* @Auther: jiliugang
* @Date: 2020/9/18 14:18
* @Description:
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsService userDetailsService;
private RedisTemplate redisTemplate = null;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(SmsCodeAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("SmsCodeAuthenticationProvider.onlySupports", "Only SmsCodeAuthenticationToken is supported");
});
SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
UserServiceImpl userService = null;
if (UserServiceImpl.class.isInstance(userDetailsService)){
userService = (UserServiceImpl) userDetailsService;
}
String mobile = (String) authentication.getPrincipal();
String code = smsCodeAuthenticationToken.getCode();
Object o = redisTemplate.opsForValue().get("sms:" + mobile);
if (o == null) {
throw new InternalAuthenticationServiceException("找不到验证信息");
}
if (!code.equals(o.toString())){
throw new BadCredentialsException("验证码错误");
}
UserDetails loadedUser = userService.loadUserByMobileSms((String)authentication.getPrincipal(),smsCodeAuthenticationToken.getCode());
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("获取不到用户信息");
}
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(loadedUser,loadedUser.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public void setRedisTemplate(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
}
到这里我们需要自定义的类已经弄好了,但是还没有加入到security管理流程中去,下面我们就把这些加入进去
扩展一个MyAuthenticationSecurityConfig配置类交给spring容器去管理
把刚才我们自定义的Filter 和provider加入到配置中去
/**
* @Auther: jiliugang
* @Date: 2020/9/18 16:20
* @Description:
*/
@Component
public class MyAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
AuthenticationManager sharedObject = http.getSharedObject(AuthenticationManager.class);
smsCodeAuthenticationFilter.setAuthenticationManager(sharedObject);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsAuthenticationFailureHandler());
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setRedisTemplate(redisTemplate);
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider);
http.addFilterBefore(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
然后我们需要在securi配置中加上扩展的配置
/**
* @Auther: jiliugang
* @Date: 2020/9/4 15:59
* @Description:
*/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private AuthenticationManager authenticationManager;
@Autowired
private MyAuthenticationSecurityConfig myAuthenticationSecurityConfig;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
authenticationManager = super.authenticationManagerBean();
return authenticationManager;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/user/login.html","/mobileAndCode/login","/user/sms").permitAll()
.antMatchers("/user/login","/login/**","/**.html").permitAll()
.antMatchers("/css/**", "/js/**",
"/plugins/**","/static/**","/imgs/**","/*+.ico")
.permitAll()
.anyRequest().authenticated()
.and().formLogin()
.failureHandler(new FormAuthenticationFailureHandler())
.loginPage("/user/login.html")
.loginProcessingUrl("/user/login")
.and().apply(myAuthenticationSecurityConfig);
}
}
至此,整个配置流程就已经完成
启动项目测试
首先访问,获取验证码(注意,这里为了测试方便改为手动设置验证码)
登陆测试
访问
跳转登录
输入刚才的 账号 验证码
登陆成功!!!