Spring全家桶-Spring Security之图片验证码
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
为什么需要验证码?
在验证用户名和密码之前,引入辅助验证可有效防范暴力试错,图形验证码就是简单且行之有效的一种辅助验证方式,还有滑块验证码,机器人验证等方式。
一、验证码是什么?
全自动区分计算机和人类的公开图灵测试(英语:Completely Automated Public Turing test to tell Computers and Humans Apart,简称CAPTCHA),又称验证码,是一种区分用户是机器或人类的公共全自动程序。在CAPTCHA测试中,作为服务器的计算机会自动生成一个问题由用户来解答。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于机器无法解答CAPTCHA的问题,回答出问题的用户即可视为人类。---《维基百科》
二、通过过滤器实现验证码
自定义一个专门处理验证码逻辑的过滤器,将其添加到Spring Security过滤器链的合适位置。当匹配到登录请求时,立刻对验证码进行校验,成功则放行,失败则提前结束整个验证请求。
使用步骤
构建项目spring-security-captcha-filter
我们使用com.google.code.kaptcha
进行验证码的生成。
官网地址:https://code.google.com/archive/p/kaptcha/wikis
项目的pom文件如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.code</groupId>
<artifactId>kaptcha</artifactId>
</dependency>
1. 创建一个验证码的Filter
public class CaptchaFilter extends OncePerRequestFilter {
//spring security登陆接口地址
public static final String LOGIN_URL = "/login";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//只有登陆的情况下需要校验验证码
if(!Objects.equals(LOGIN_URL,request.getRequestURI())){
filterChain.doFilter(request,response);
}else{
//校验验证码
checkCaptcha(request);
filterChain.doFilter(request,response);
}
}
//校验验证码
private void checkCaptcha(HttpServletRequest request) {
String captcha = request.getParameter("captcha");
HttpSession session = request.getSession();
String catpchaSave = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(StringUtils.hasLength(catpchaSave)){
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
}
if(!StringUtils.hasLength(captcha) || !StringUtils.hasLength(catpchaSave) || !Objects.equals(captcha,catpchaSave)){
throw new RuntimeException("验证码输入错误");
}
}
}
2.创建一个验证码的Bean
@Bean
public Producer captchaProducer(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","150");
properties.setProperty("kaptcha.image.height","15");
properties.setProperty("kaptcha.textproducer.char.string","0123456789");
properties.setProperty("kaptcha.textproducer.char.length","4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
3.创建验证码请求
@Controller
public class CaptchaController {
@Autowired
private Producer captchaProducer;
@GetMapping("/captcha")
public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");
response.setContentType("image/jpeg");
String capText = captchaProducer.createText();
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);
BufferedImage bi = captchaProducer.createImage(capText);
ServletOutputStream out = response.getOutputStream();
// write the data out
ImageIO.write(bi, "jpg", out);
try (out) {
ImageIO.write(bi, "jpg", out);
out.flush();
}
}
}
4.登陆页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>验证码登陆</title>
</head>
<body>
<form action="/login" method="post">
<label>用户名:</label>
<label>
<input type="text" name="username" />
</label>
<label>密码:</label>
<label>
<input type="password" name="password" />
</label>
<label>验证码:</label>
<label>
<input type="text" name="captcha" />
<img id="captcha" src="/captcha" alt="captcha" height="50px" width="150px" style="margin-left: 15px;" onclick="refreshCaptcha()">
</label>
<button type="submit" >登陆</button>
</form>
</body>
<script>
//刷新验证码
function refreshCaptcha(){
var img = document.getElementById("captcha");
img.src = "/captcha";
}
</script>
</html>
运行验证
运行项目,访问http://localhost:8080/login.html
,将出现如下页面:
输入默认的用户名和密码/验证码即可登陆,如果验证码不正确,将会出现相关的报错:
java.lang.RuntimeException: 验证码输入错误
at org.tony.spring.security.config.CaptchaFilter.checkCaptcha(CaptchaFilter.java:47) ~[classes/:na]
at org.tony.spring.security.config.CaptchaFilter.doFilterInternal(CaptchaFilter.java:34) ~[classes/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar:5.3.18]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.6.2.jar:5.6.2]
三、自定义认证实现验证码
使用步骤
创建项目spring-security-captcha-authentication
- 创建项目的pom
项目的pom和上面项目的pom文件一样
2.创建相关的bean
//配置验证码bean
@Bean
public Producer captchaProducer(){
Properties properties = new Properties();
properties.setProperty("kaptcha.image.width","150");
properties.setProperty("kaptcha.image.height","50");
properties.setProperty("kaptcha.textproducer.char.string","0123456789");
properties.setProperty("kaptcha.textproducer.char.length","4");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
//创建userDetailsService
@Bean
public UserDetailsService userDetailsService(){
return new UserDetailsServiceImpl();
}
//设置密码加密策略
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
3.创建UserDetailsService
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo userInfo = new UserInfo();
userInfo.setUsername("admin");
//设置相关密码,这里的密码一定要和加密策略中设置的密码一致
userInfo.setPassword(new BCryptPasswordEncoder().encode("123456"));
return userInfo;
}
}
4.创建用户实体
public class UserInfo implements UserDetails {
//密码
private String password;
//用户名
private String username;
public void setPassword(String password) {
this.password = password;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_ADMIN");
List<GrantedAuthority> authorities =new ArrayList<>();
authorities.add(authority);
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
以上是创建相关的实体和UserDetailService
,和相关的密码策略。
5.创建CustomeAuthenticationProvider
认证
代码如下:
@Component
public class CustomeAuthenticationProvider extends DaoAuthenticationProvider {
//这个地方将UserDetailsService和PasswordEncoder通过构造进行添加,这边要进行相关的定义和创建相应的Bean
public CustomeAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder){
this.setUserDetailsService(userDetailsService);
this.setPasswordEncoder(passwordEncoder);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
//获取详细信息
CustomWebAuthenticationDetails customWebAuthenticationDetails = (CustomWebAuthenticationDetails) authentication.getDetails();
//校验相应的验证码正确性
if(!Boolean.TRUE.equals(customWebAuthenticationDetails.getCaptchaCheck())){
throw new RuntimeException("验证码不正确");
}
super.additionalAuthenticationChecks(userDetails,authentication);
}
}
创建CustomWebAuthenticationDetails
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
//验证码是否校验成功标记
private Boolean captchaCheck;
public Boolean getCaptchaCheck() {
return this.captchaCheck;
}
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
String captcha = request.getParameter("captcha");
HttpSession session = request.getSession();
String captchaFromSession = (String) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(StringUtils.hasLength(captchaFromSession)){
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
if(StringUtils.hasLength(captcha) && Objects.equals(captcha,captchaFromSession)){
this.captchaCheck = true;
}else{
this.captchaCheck = false;
}
}
}
}
创建CustomWebAuthenticationSource
@Component
public class CustomWebAuthenticationSource implements
AuthenticationDetailsSource<HttpServletRequest,WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new CustomWebAuthenticationDetails(request);
}
}
修改配置类WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> customWebAuthenticationSource;
//应用AuthenticationProvider,通过自定义AuthenticationProvider进行验证码验证
@Autowired
private AuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.antMatchers("/books/**").hasAnyRole("ADMIN")
.antMatchers("/", "/captcha").permitAll()
.anyRequest().authenticated()
.and()
//使用customWebAuthenticationSource
.formLogin().authenticationDetailsSource(customWebAuthenticationSource).loginPage("/login.html").loginProcessingUrl("/login").permitAll()
.and()
.csrf().disable();
}
}
运行验证
运行项目,访问http://localhost:8080/login.html
,将出现如下页面:
输入默认的用户名和密码/验证码即可登陆,如果验证码不正确,将会出现相关的报错:
java.lang.RuntimeException: 验证码输入错误
at org.tony.spring.security.config.CaptchaFilter.checkCaptcha(CaptchaFilter.java:47) ~[classes/:na]
at org.tony.spring.security.config.CaptchaFilter.doFilterInternal(CaptchaFilter.java:34) ~[classes/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar:5.3.18]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:336) ~[spring-security-web-5.6.2.jar:5.6.2]
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-5.6.2.jar:5.6.2]
以上是实现验证码的两种方式,使用过滤器是比较简单的实现,属于Servlet层面, 简单、 易理解。 其实, Spring Security还提供了一种更优雅的实现图形验证码的方式, 即自定义认证。
总结
我们都知道,Spring Security
是通过过滤器链进行不同的过滤器拦截。我们在HttpSecurity
中可以配置过滤器,如CSRF、 CORS、 表单登录等。每一个配置对应一个过滤器链。所有我们通过过滤器可以做到验证码的校验拦截。我们创建拦截器继承OncePerRequestFilter
进行清关的处理。
OncePerRequestFilter
旨在保证在任何servlet容器上每请求分派一次执行的过滤器基类。它提供了一个doFilterInternal(javax.servlet.http。HttpServletRequest javax.servlet.http。HttpServletResponse, javax.servlet.FilterChain)
方法,带有HttpServletRequest和HttpServletResponse参数.
子类可以使用isAsyncDispatch(httpservletrequest)
来确定何时调用过滤器作为异步调度的一部分,并使用isAsyncStarted(HttpServletRequest request)
来确定何时将请求放置在异步模式下,因此当前派遣派遣何时是异常派遣模式对于给定的请求。在其自身线程中也出现的另一种调度类型是ERROR
。如果子类希望在错误派遣期间应调用一次,则可以替代他们.
我们所面对的系统中的用户, 在Spring Security
中被称为主体(principal) 。主体包含了所有能够经过验证而获得系统访问权限的用户、 设备或其他系统。 主体的概念实际上来自 Java Security
,SpringSecurity
通过一层包装将其定义为一个Authentication
。
public interface Authentication extends Principal, Serializable {
//获取权限列表
Collection<? extends GrantedAuthority> getAuthorities();
//获取凭据
Object getCredentials();
//获取详细信息
Object getDetails();
//获取主体
Object getPrincipal();
//是否认证成功
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
由于大部分场景下身份验证都是基于用户名和密码进行的,因此Spring Security为我们提供了UsernamePasswordAuthenticationToken
进行用户和密码的认证。在使用的表单登录中, 每一个登录用户都被包装为一个 ·UsernamePasswordAuthenticationToken·, 从而在Spring Security的各个AuthenticationProvider
中进行使用。
public interface AuthenticationProvider {
//认证成功,返回认证信息
Authentication authenticate(Authentication authentication) throws AuthenticationException;
//是否支持验证当前的Authentication
boolean supports(Class<?> authentication);
}
一次完整的认证可以包含多个AuthenticationProvider
。通过ProviderManager
进行管理。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();
//迭代验证每个AuthenticationProvider,直到有一个验证通过
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
Spring Security
提供了多种常见的认证技术, 包括但不限于以下几种:
- HTTP层面的认证, 包括HTTP基本认证和HTTP摘要认证
- 基于LDAP认证
- 证明用户身份的OpenID认证
- 授权的OAuth认证
- 基于数据库用户名和密码认证
Spring Security
为我们提供和一个抽象的认证AbstractUserDetailsAuthenticationProvider
.
在AbstractUserDetailsAuthenticationProvider
中实现了基本的认证流程, 通过继承AbstractUserDetailsAuthenticationProvider
, 并实现retrieveUser
和additionalAuthenticationChecks
两个抽象方法即可自定义核心认证过程.
问题
通过maven下载不了验证码的jar包?
解决:通过取私服上下载下来,之后解压到指定文件夹中,之后执行maven命令就可以将相关的jar安装到本地仓库中了。命令如下:
mvn install:install-file -DgroupId=com.google.code -DartifactId=kaptcha -Dversion=2.3.2 -Dfile=/Users/xiell/Downloads/kaptcha-2.3.2.jar -Dpackaging=jar -DgeneratePom=true