在使用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
* AuthenticationProvider
s) 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;
}
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));
}
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;
}
@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"));
}
}
为了实现图形验证码自定义认证,我们需要做如何几件事
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;
}
}
public class CaptchaWebAuthenticationDetailsSource extends WebAuthenticationDetailsSource {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
return new CaptchaWebAuthenticationDetails(request);
}
}
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);
}
}
@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,重定向到登录页面
账号/密码,验证码输入正确,点击Login进入http://localhost:8080/admin/api/hello页面