需求,默认的SpringSecurity密码校验,无法满足需求,需要自定义来进行密码比对
a、网上找了一些教程,没法一步到位,很多代码,连import都没有贴出来,烦死了。
b、这里说明一下,我是为了图方便,系统原有密码登录情况下,要加入验证码登录,由于验证码时4-5位纯数字,而密码是6位以上的数字+字母,因此不想Filter这些全部来搞,因此想了这个办法简洁一点,也很快能够完成功能。
c、主要涉及两个地方,1是新增MyAuthenticationProvider类,继承DaoAuthenticationProvider,2,public class SecurityConfig extends WebSecurityConfigurerAdapter配置类中修改protected void configure(AuthenticationManagerBuilder auth) throws Exception方法。
MyAuthenticationProvider.java代码如下:
import com.dhproject.common.constant.CacheConstants;
import com.dhproject.common.core.redis.RedisCache;
import com.dhproject.common.exception.ServiceException;
import com.dhproject.common.utils.SecurityUtils;
import com.dhproject.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
public class MyAuthenticationProvider extends DaoAuthenticationProvider {
//passwordEncoder密码比较器
@Resource
private PasswordEncoder passwordEncoder;
//Redis
@Autowired
protected RedisCache redisCache;
/**
* 构造初始化
* @param userDetailsService
* @param passwordEncoder
* @param redisCache
*/
public MyAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, RedisCache redisCache) {
super();
// 这个地方一定要对userDetailsService赋值,不然userDetailsService是null (这个坑有点深)
setUserDetailsService(userDetailsService);
//passwordEncoder由于父类是private,这里需要自定义初始化后才能使用
this.passwordEncoder = passwordEncoder;
//这个是Redis,根据实际情况初始化
this.redisCache = redisCache;
}
/**
* 重写该方法
* @param userDetails
* @param authentication
* @throws AuthenticationException
*/
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String mobile = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
//这里进行密码和验证码验证
//如果是四位纯数字,那么认定是验证码登录
if(isCodeLogin(password)){
// 1. 检验Redis手机号的验证码
String verifyKey = CacheConstants.SMS_CODE_KEY+mobile;
String code = redisCache.getCacheObject(verifyKey);
if (StringUtils.isEmpty(code)) {
throw new ServiceException("验证码已经过期或尚未发送,请重新发送验证码");
}
if (!password.equals(code)) {
throw new ServiceException("输入的验证码不正确,请重新输入");
}
// 2. 短信验证成功后要删除redis中的验证码
redisCache.deleteObject(verifyKey);
}else{
if (!this.passwordEncoder.matches(password, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
}
/**
* 时都是验证码登录
* @param password
* @return
*/
public boolean isCodeLogin(String password) {
return StringUtils.isNumeric(password) && (password.length()==4||password.length()==5);
}
}
SecurityConfig.java代码如下:一定要注意看说明,不然肯定最后还会有问题
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//定义了新的Provider方法,就不能使用默认的userDetailsService进行构造,否则抛出BadCredentialsException异常,代码会继续执行
auth.authenticationProvider(new MyAuthenticationProvider(userDetailsService,bCryptPasswordEncoder(),redisCache));
//默认的userDetailsService进行构造
//auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
SecurityConfig.java类,全部文件,我也贴一下,方便各位参考:
import com.dhproject.common.core.redis.RedisCache;
import com.dhproject.framework.security.sms.MyAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.dhproject.framework.config.properties.PermitAllUrlProperties;
import com.dhproject.framework.security.filter.JwtAuthenticationTokenFilter;
import com.dhproject.framework.security.handle.AuthenticationEntryPointImpl;
import com.dhproject.framework.security.handle.LogoutSuccessHandlerImpl;
/**
* spring security配置
*
* @author
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Autowired
protected RedisCache redisCache;
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage", "/container/public/sendSms").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**", "/public/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 添加CORS filter
httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//定义了新的Provider方法,就不能使用默认的userDetailsService进行构造,否则抛出BadCredentialsException异常,代码会继续执行
auth.authenticationProvider(new MyAuthenticationProvider(userDetailsService,bCryptPasswordEncoder(),redisCache));
//默认的userDetailsService进行构造
//auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
2.可能存在问题问题
MyDaoAuthenticationProvider重写DaoAuthenticationProvider中additionalAuthenticationChecks方法后,自定义验证密码错误后,抛出BadCredentialsException异常,代码会继续执行,会执行DaoAuthenticationProvider中的additionalAuthenticationChecks发放,导致自定义抛出的BadCredentialsException的异常无用。
代码注释中已经说明