springboot集成security(手机号+验证码)
西风吹老洞庭波,
一夜湘君白发多。
醉后不知天在水,
满船清梦压星河。
主要基于用户名密码的方式进行改造,不知道的可以先看一下:springboot集成security(前后端分离 用户名密码登陆)
大概思路就是我们要改变security的验证方式,需要去实现AuthenticationProvider自定义一个token给security,改成我们自定义的验证方式,而不是使用账号密码的方式
SmsCodeAuthenticationProvider
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {private SecurityUserServiceImpl userDetailsService;private RedisTemplate<String, Object> redisTemplate;public SmsCodeAuthenticationProvider(SecurityUserServiceImpl userService, RedisTemplate redisTemplate) {this.userDetailsService = userService;this.redisTemplate = redisTemplate;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;// 验证码校验String phone = (String) authenticationToken.getPrincipal();checkCode(phone);UserDetails user = userDetailsService.loadUserByUsername(phone);if (Objects.isNull(user)) {throw new InternalAuthenticationServiceException("用户名或密码错误!");}SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> authentication) {return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}private void checkCode(String phone) {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String inputCode = request.getParameter("captcha");Object code = redisTemplate.opsForValue().get(RedisKeyConstant.CAPTCHA + phone);if (ObjectUtils.isEmpty(code)){throw new CredentialsExpiredException("验证码过期,请重新获取!");}if(!Objects.equals(code, inputCode)) {throw new BadCredentialsException("验证码错误");}}}
我是用阿里云发送的短信,然后存redis的,这个不用多说
验证码过滤器SmsCodeAuthenticationFilter:
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {private boolean postOnly = true;private String mobileParameter = "phone";public SmsCodeAuthenticationFilter() {super(new AntPathRequestMatcher("/phone/login", HttpMethod.POST.name()));}public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {if (postOnly && !HttpMethod.POST.name().equals(request.getMethod())) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}// 获取请求中的参数值String mobile = request.getParameter("phone");if (Objects.isNull(mobile)) {mobile = "";}mobile = mobile.trim();SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {authRequest.setDetails(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 getMobileParameter() {return mobileParameter;}}
修改SecurityUserServiceImpl:
@Overridepublic UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {SysUser sysUser = userService.getUserByPhone(phone);if (ObjectUtils.isEmpty(sysUser)) {throw new UsernameNotFoundException("用户名或密码不正确");}return new LoginUser(sysUser.getId().longValue(), sysUser.getName(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));}
修改loadUserByUsername方法,之前是通过用户名查的,现在要通过手机号查
修改JwtAuthenticationFilter:
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {String token = request.getHeader("Authorization");if(!StringUtils.hasText(token)){chain.doFilter(request,response);return;}Claims claim = jwtUtils.getClaimByToken(token);if(ObjectUtils.isEmpty(claim)){throw new JwtException("非法请求");}String username = claim.get("username").toString();Object redisToken = redisTemplate.opsForValue().get(RedisKeyConstant.USER_TOKEN + ":" + username);if (ObjectUtils.isEmpty(redisToken)){throw new AccountExpiredException("登录过期");}// 重置过期时间redisTemplate.opsForValue().set(RedisKeyConstant.USER_TOKEN + ":" + username, token, 30, TimeUnit.MINUTES);SysUser sysUser = userService.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getName, username));if (ObjectUtils.isEmpty(sysUser)){throw new CustomerException("非法请求");}
// UsernamePasswordAuthenticationToken authenticationToken =
// new UsernamePasswordAuthenticationToken(sysUser,null, securityUserService.getUserAuthority(sysUser.getId()));SmsCodeAuthenticationToken smsAbstractAuthenticationToken = new SmsCodeAuthenticationToken(sysUser, securityUserService.getUserAuthority(sysUser.getId()));SecurityContextHolder.getContext().setAuthentication(smsAbstractAuthenticationToken);chain.doFilter(request,response);}
之前是UsernamePasswordAuthenticationToken用户名密码方式的token,要改成我们自定义的短信验证方式的token
新加secruity的配置SmsCodeAuthenticationSecurityConfig:
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Resourceprivate SecurityUserServiceImpl userDetailsService;@Resourceprivate LoginSuccessHandler customAuthenticationSuccessHandler;@Resourceprivate LoginFailureHandler customAuthenticationFailureHandler;@Resourceprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void configure(HttpSecurity http) throws Exception {SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);smsCodeAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(userDetailsService, redisTemplate);http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}}
之前SecurityConfig里面配置额不要的东西可以删掉:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Resourceprivate JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;@Resourceprivate JwtAccessDeniedHandler jwtAccessDeniedHandler;@Resourceprivate SecurityUserServiceImpl securityUserService;@Resourceprivate LoginFailureHandler loginFailureHandler;@Resourceprivate LoginSuccessHandler loginSuccessHandler;@Resourceprivate SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;@BeanBCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}@BeanJwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());return jwtAuthenticationFilter;}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}private static final String[] URL_WHITELIST = {"/login","/phone/login","/logout","/login/captcha","/favicon.ico",};
// protected void configure(HttpSecurity http) throws Exception {
// http.cors().and().csrf().disable()
// .apply(smsCodeAuthenticationSecurityConfig).and().authorizeRequests().and()
// //登录配置
// .formLogin()
// .successHandler(loginSuccessHandler)
// .failureHandler(loginFailureHandler)
//
// //禁用session
// .and()
// .sessionManagement()
// .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// //配置拦截规则
// .and()
// .authorizeRequests()
// .antMatchers(URL_WHITELIST).permitAll()
// .anyRequest().authenticated()
// //异常处理器
// .and()
// .exceptionHandling()
// .authenticationEntryPoint(jwtAuthenticationEntryPoint)
// .accessDeniedHandler(jwtAccessDeniedHandler)
// //配置自定义的过滤器
// .and()
// .addFilter(jwtAuthenticationFilter())
// ;
// }@Overrideprotected void configure(HttpSecurity http) throws Exception {http.apply(smsCodeAuthenticationSecurityConfig).and().authorizeRequests()// 如果有允许匿名的url,填在下面.antMatchers(URL_WHITELIST).permitAll().anyRequest().authenticated().and()// 设置登陆页.formLogin()// 设置登陆成功页.defaultSuccessUrl("/").permitAll().and().logout().permitAll();// 关闭CSRF跨域http.csrf().disable();}
这里注意配置一下登录和发送验证码的白名单
到这里就集成完了,可能并不完美,还需要根据自己的项目进行优化,后续我也会持续优化