springboot——security实现验证码:使用自定义认证

在使用Spring Security的自定义认证之前,有必要了解Spring Security是如何灵活集成多种认证方式的。在spring Security中用户被称为主体(principal),主体包含了所有能够验证而获得系统访问权限的用户、设备或其他系统。主体的概念来自Java Security,自定义认证的基类是Authentication

public interface Authentication extends Principal, Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Set by an AuthenticationManager to indicate the authorities that the
	 * principal has been granted. Note that classes should not rely on this value as
	 * being valid unless it has been set by a trusted AuthenticationManager.
	 * 

* Implementations should ensure that modifications to the returned collection array * do not affect the state of the Authentication object, or use an unmodifiable * instance. *

* * @return the authorities granted to the principal, or an empty collection if the * token has not been authenticated. Never null. */
// 权限列表 Collection<? extends GrantedAuthority> getAuthorities(); /** * The credentials that prove the principal is correct. This is usually a password, * but could be anything relevant to the AuthenticationManager. Callers * are expected to populate the credentials. * * @return the credentials that prove the identity of the Principal */ // 密码 Object getCredentials(); /** * Stores additional details about the authentication request. These might be an IP * address, certificate serial number etc. * * @return additional details about the authentication request, or null * if not used */ // 其他信息 Object getDetails(); /** * The identity of the principal being authenticated. In the case of an authentication * request with username and password, this would be the username. Callers are * expected to populate the principal for an authentication request. *

* The AuthenticationManager implementation will often return an * Authentication containing richer information as the principal for use by * the application. Many of the authentication providers will create a * {@code UserDetails} object as the principal. * * @return the Principal being authenticated or the authenticated * principal after authentication. */ // 用户名 Object getPrincipal(); /** * Used to indicate to {@code AbstractSecurityInterceptor} whether it should present * the authentication token to the AuthenticationManager. Typically an * AuthenticationManager (or, more often, one of its * AuthenticationProviders) will return an immutable authentication token * after successful authentication, in which case that token can safely return * true to this method. Returning true will improve * performance, as calling the AuthenticationManager for every request * will no longer be necessary. *

* For security reasons, implementations of this interface should be very careful * about returning true from this method unless they are either * immutable, or have some way of ensuring the properties have not been changed since * original creation. * * @return true if the token has been authenticated and the * AbstractSecurityInterceptor does not need to present the token to the * AuthenticationManager again for re-authentication. */ // 是否验证成功 boolean isAuthenticated(); /** * See {@link #isAuthenticated()} for a full description. *

* Implementations should always allow this method to be called with a * false parameter, as this is used by various classes to specify the * authentication token should not be trusted. If an implementation wishes to reject * an invocation with a true parameter (which would indicate the * authentication token is trusted - a potential security risk) the implementation * should throw an {@link IllegalArgumentException}. * * @param isAuthenticated true if the token should be trusted (which may * result in an exception) or false if the token should not be trusted * * @throws IllegalArgumentException if an attempt to make the authentication token * trusted (by passing true as the argument) is rejected due to the * implementation being immutable or implementing its own alternative approach to * {@link #isAuthenticated()} */ void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }

一、分析UsernamePasswordAuthenticationFilter#attemptAuthentication源码

public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
		
		// 获取用户名和密码
		String username = obtainUsername(request);
		String password = obtainPassword(request);

		if (username == null) {
			username = "";
		}

		if (password == null) {
			password = "";
		}

		username = username.trim();
	
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		// 1.放置其他认证信息到Authentication中
		setDetails(request, authRequest);
		
		// 3. 这个可以看到是通过AuthenticationManager->ProviderManager实际管理认证过程
		return this.getAuthenticationManager().authenticate(authRequest);
	}
    
    /**
	 * Provided so that subclasses may configure what is put into the authentication
	 * request's details property.
	 *
	 * @param request that an authentication request is being created for
	 * @param authRequest the authentication request object that should have its details
	 * set
	 */
	protected void setDetails(HttpServletRequest request,
			UsernamePasswordAuthenticationToken authRequest) {
		// 2. 通过authenticationDetailsSource进行组装Detail
		// 默认情况是WebAuthenticationDetailsSource记录的remoteIp和sessionId
		// 由此我们只要扩展WebAuthenticationDetailsSource就可以将验证码信息记录下来
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

二、分析ProviderManager#authenticate源码

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;
		boolean debug = logger.isDebugEnabled();
		
		// 4. 由此可以看到他管理着各种AuthenticationProvider
		// DaoAuthenticationProvider就是其中一种验证方式
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

三、分析DaoAuthenticationProvider#additionalAuthenticationChecks源码

@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
			
		// 我们只需要在此验证验证码是否正确
		// 由此我们只需要继承DaoAuthenticationProvider即可

		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}

为了实现图形验证码自定义认证,我们需要做如何几件事

  1. 定义CaptchaWebAuthenticationDetails继承WebAuthenticationDetails添加验证码信息;
  2. 定义CaptchaWebAuthenticationDetailsSource继承WebAuthenticationDetailsSource;
  3. 定义CaptchaDaoAuthenticationProvider继承DaoAuthenticationProvider
  4. 配置CaptchaWebAuthenticationDetailsSource和CaptchaDaoAuthenticationProvider

四、定义CaptchaWebAuthenticationDetails

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Objects;

public class CaptchaWebAuthenticationDetails extends WebAuthenticationDetails {
    private final boolean captchaCodeIsRight;
    public CaptchaWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        String captcha = request.getParameter("captcha");
        HttpSession session = request.getSession();
        String expected = (String) session.getAttribute("captcha");
        captchaCodeIsRight = Objects.equals(captcha, expected);
    }

    public boolean isCaptchaCodeIsRight() {
        return captchaCodeIsRight;
    }
}

五、定义CaptchaWebAuthenticationDetails

public class CaptchaWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CaptchaWebAuthenticationDetails(request);
    }
}

六、定义CaptchaDaoAuthenticationProvider

public class CaptchaDaoAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        CaptchaWebAuthenticationDetails details  = (CaptchaWebAuthenticationDetails)authentication.getDetails();
        if (!details.isCaptchaCodeIsRight()) {
            throw new VerifationCodeException("验证码错误!");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);
    }
}

七、定义CaptchaDaoAuthenticationProvider

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	super.configure(auth);
	auth.authenticationProvider(daoAuthenticationProvider);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
			.antMatchers("/admin/api/**").hasRole("ADMIN")
			.antMatchers("/user/api/**").hasRole("USER")
			.antMatchers("/app/api/**", "/captcha.jpg", "/login.html").permitAll()
			.anyRequest().authenticated()
			.and()
			.formLogin()
			.authenticationDetailsSource(webAuthenticationDetailsSource)
			.loginPage("/login.html")
			.loginProcessingUrl("/login")
			.failureHandler(authenticationFailureHandler())
			.and().sessionManagement().maximumSessions(1)
			.and().and()
			.csrf().disable();
}

@Bean
public PasswordEncoder passwordEncoder() {
	return new MessageDigestPasswordEncoder("MD5");
}

@Bean
public ObjectMapper objectMapper() {
	return new ObjectMapper();
}

@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
	return (request, response, exception) -> {
		Map<String, Object> map = new HashMap<>();
		map.put("code", 401);
		map.put("message", "验证码错误");
		response.setContentType("application/json;charset=utf-8");
		PrintWriter out = response.getWriter();
		out.write(new ObjectMapper().writeValueAsString(map));
		out.flush();
		out.close();
	};
}

@Bean
public WebAuthenticationDetailsSource webAuthenticationDetailsSource() {
	return new CaptchaWebAuthenticationDetailsSource();
}

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider(UserDetailsService userDetailsService) {
	CaptchaDaoAuthenticationProvider captchaDaoAuthenticationProvider = new CaptchaDaoAuthenticationProvider();
	captchaDaoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
	captchaDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
	return captchaDaoAuthenticationProvider;
}

启动springboot,访问http://localhost:8080/admin/api/hello,重定向到登录页面
springboot——security实现验证码:使用自定义认证_第1张图片
账号/密码,验证码输入正确,点击Login进入http://localhost:8080/admin/api/hello页面

八、注意事项

  1. 项目具体配置请参考springboot——security实现验证码:自定义过滤器

你可能感兴趣的:(springboot)