前言
在SpringSecurity的默认登录反对组件formLogin中没有提供图形验证码的反对,目前大多数的计划都是通过新增Filter来实现。filter的形式能够实现性能,然而没有优雅的解决, 须要从新保护一套和登录相干的url,例如:loginProccessUrl,loginFailUrl,loginSuccessUrl,从软件设计角度来讲性能没有内聚。
上面为大家介绍一种优雅的解决方案。
解决思路
先获取验证码
判断图形验证码先要获取到验证码,在UsernamePasswordAuthenticationToken(UPAT)中没有字段来存储验证码,重写UPAT老本太高。能够从details字段中动手,将验证码放在details中。
判断验证码是否正确
UPAT的认证是在DaoAuthenticationProvider中实现的,如果须要判断验证码间接批改是老本比拟大的形式,能够新增AuthenticationProvider来对验证码新增验证。
输入验证码
惯例超过能够通过Controller来输入,然而验证码的治理须要对立,避免各种sessionKey乱飞。
代码实现
新增验证码容器:CaptchaAuthenticationDetails
public class CaptchaAuthenticationDetails extends WebAuthenticationDetails {
private final String DEFAULT_CAPTCHA_PARAMETER_NAME = "captcha";
private String captchaParameter = DEFAULT_CAPTCHA_PARAMETER_NAME;
/**
* 用户提交的验证码
*/
private String committedCaptcha;
/**
* 预设的验证码
*/
private String presetCaptcha;
private final WebAuthenticationDetails webAuthenticationDetails;
public CaptchaAuthenticationDetails(HttpServletRequest request) {
super(request);
this.committedCaptcha = request.getParameter(captchaParameter);
this.webAuthenticationDetails = new WebAuthenticationDetails(request);
}
public boolean isCaptchaMatch() {
if (this.presetCaptcha == null || this.committedCaptcha == null) {
return false;
}
return this.presetCaptcha.equalsIgnoreCase(committedCaptcha);
}
getter ...
setter ...
}
这个类次要是用于保留验证码
验证码获取:CaptchaAuthenticationDetailsSource
public class CaptchaAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, CaptchaAuthenticationDetails> {
private final CaptchaRepository<HttpServletRequest> captchaRepository;
public CaptchaAuthenticationDetailsSource(CaptchaRepository<HttpServletRequest> captchaRepository) {
this.captchaRepository = captchaRepository;
}
@Override
public CaptchaAuthenticationDetails buildDetails(HttpServletRequest httpServletRequest) {
CaptchaAuthenticationDetails captchaAuthenticationDetails = new CaptchaAuthenticationDetails(httpServletRequest);
captchaAuthenticationDetails.setPresetCaptcha(captchaRepository.load(httpServletRequest));
return captchaAuthenticationDetails;
}
}
依据提交的参数构建CaptchaAuthenticationDetails,用户提交的验证码(committedCaptcha)从request中获取,预设的验证码(presetCaptcha)从验证码仓库(CaptchaRepostory)获取
验证码仓库实现SessionCaptchaRepository
public class SessionCaptchaRepository implements CaptchaRepository<HttpServletRequest> {
private static final String CAPTCHA_SESSION_KEY = "captcha";
/**
* the key of captcha in session attributes */ private String captchaSessionKey = CAPTCHA_SESSION_KEY;
@Override
public String load(HttpServletRequest request) {
return (String) request.getSession().getAttribute(captchaSessionKey);
}
@Override
public void save(HttpServletRequest request, String captcha) {
request.getSession().setAttribute(captchaSessionKey, captcha);
}
/**
* @return sessionKey */ public String getCaptchaSessionKey() {
return captchaSessionKey;
}
/**
* @param captchaSessionKey sessionKey
*/ public void setCaptchaSessionKey(String captchaSessionKey) {
this.captchaSessionKey = captchaSessionKey;
}
}
这个验证码仓库是基于Session的,如果想要基于Redis只有实现CaptchaRepository即可。
对验证码进行认证CaptchaAuthenticationProvider
public class CaptchaAuthenticationProvider implements AuthenticationProvider {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;
CaptchaAuthenticationDetails details = (CaptchaAuthenticationDetails) authenticationToken.getDetails();
if (!details.isCaptchaMatch()) {
//验证码不匹配抛出异样,退出认证
if (log.isDebugEnabled()) {
log.debug("认证失败:验证码不匹配");
}
throw new CaptchaIncorrectException("验证码谬误");
}
//替换details
authenticationToken.setDetails(details.getWebAuthenticationDetails());
//返回空交给下一个provider进行认证
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
}
SpringSecurity配置
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public CaptchaRepository<HttpServletRequest> sessionCaptchaRepository() {
return new SessionCaptchaRepository();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginProcessingUrl("/login")
.loginPage("/login.html")
.authenticationDetailsSource(new CaptchaAuthenticationDetailsSource(sessionCaptchaRepository()))
.failureUrl("/login.html?error=true")
.defaultSuccessUrl("/index.html")
.and()
.authenticationProvider(new CaptchaAuthenticationProvider())
.csrf()
.disable();
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.mvcMatchers("/captcha", "/login.html");
}
}
将CaptchaAuthenticationProvider退出到认证链条中,重新配置authenticationDetailsSource
提供图形验证码接口
@Controller
public class CaptchaController {
private final CaptchaRepository<HttpServletRequest> captchaRepository;
@Autowired
public CaptchaController(CaptchaRepository<HttpServletRequest> captchaRepository) {
this.captchaRepository = captchaRepository;
}
@RequestMapping("/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
RandomCaptcha randomCaptcha = new RandomCaptcha(4);
captchaRepository.save(request, randomCaptcha.getValue());
CaptchaImage captchaImage = new DefaultCaptchaImage(200, 60, randomCaptcha.getValue());
captchaImage.write(response.getOutputStream());
}
}
将生成的随机验证码(RandomCaptcha)保留到验证码仓库(CaptchaRepository)中,并将验证码图片(CaptchaImage)输入到客户端。
至此整个图形验证码认证的全流程曾经完结。
代码仓库
https://gitee.com/leecho/spri…
发表回复