使用過 Spring Security 框架的同學應(yīng)該都見過這個提示:
壞的憑證
荚坞,對應(yīng)的英文提示為Bad credentials
虎谢。不知道是不是機翻蚁鳖,反正我看著十分別扭∮邪穑客戶拿到你做的系統(tǒng)是尖,輸入錯誤的用戶名或密碼時,看到壞的憑證
這樣的提示肯定是一頭霧水泥耀。
下面就來教大家如何定制 Spring Security 的錯誤提示信息饺汹。
分析
在 spring-security-core-5.0.7.RELEASE.jar
的 org.springframework.security
包下,有一組名為 messages[_language_country].properties
的配置文件爆袍。
messages_zh_CN.properties
內(nèi)容如下:
AbstractAccessDecisionManager.accessDenied=不允許訪問
AbstractLdapAuthenticationProvider.emptyPassword=壞的憑證
AbstractSecurityInterceptor.authenticationNotFound=未在SecurityContext中查找到認證對象
AbstractUserDetailsAuthenticationProvider.badCredentials=壞的憑證
AbstractUserDetailsAuthenticationProvider.credentialsExpired=用戶憑證已過期
AbstractUserDetailsAuthenticationProvider.disabled=用戶已失效
AbstractUserDetailsAuthenticationProvider.expired=用戶帳號已過期
AbstractUserDetailsAuthenticationProvider.locked=用戶帳號已被鎖定
AbstractUserDetailsAuthenticationProvider.onlySupports=僅僅支持UsernamePasswordAuthenticationToken
AccountStatusUserDetailsChecker.credentialsExpired=用戶憑證已過期
AccountStatusUserDetailsChecker.disabled=用戶已失效
AccountStatusUserDetailsChecker.expired=用戶帳號已過期
AccountStatusUserDetailsChecker.locked=用戶帳號已被鎖定
AclEntryAfterInvocationProvider.noPermission=給定的Authentication對象({0})根本無權(quán)操控領(lǐng)域?qū)ο?{1})
AnonymousAuthenticationProvider.incorrectKey=展示的AnonymousAuthenticationToken不含有預期的key
BindAuthenticator.badCredentials=壞的憑證
BindAuthenticator.emptyPassword=壞的憑證
CasAuthenticationProvider.incorrectKey=展示的CasAuthenticationToken不含有預期的key
CasAuthenticationProvider.noServiceTicket=未能夠正確提供待驗證的CAS服務(wù)票根
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=已經(jīng)超過了當前主體({0})被允許的最大會話數(shù)量
DigestAuthenticationFilter.incorrectRealm=響應(yīng)結(jié)果中的Realm名字({0})同系統(tǒng)指定的Realm名字({1})不吻合
DigestAuthenticationFilter.incorrectResponse=錯誤的響應(yīng)結(jié)果
DigestAuthenticationFilter.missingAuth=遺漏了針對'auth' QOP的首繁、必須給定的摘要取值; 接收到的頭信息為{0}
DigestAuthenticationFilter.missingMandatory=遺漏了必須給定的摘要取值; 接收到的頭信息為{0}
DigestAuthenticationFilter.nonceCompromised=Nonce令牌已經(jīng)存在問題了,{0}
DigestAuthenticationFilter.nonceEncoding=Nonce未經(jīng)過Base64編碼; 相應(yīng)的nonce取值為 {0}
DigestAuthenticationFilter.nonceExpired=Nonce已經(jīng)過期/超時
DigestAuthenticationFilter.nonceNotNumeric=Nonce令牌的第1部分應(yīng)該是數(shù)字陨囊,但結(jié)果卻是{0}
DigestAuthenticationFilter.nonceNotTwoTokens=Nonce應(yīng)該由兩部分取值構(gòu)成弦疮,但結(jié)果卻是{0}
DigestAuthenticationFilter.usernameNotFound=用戶名{0}未找到
JdbcDaoImpl.noAuthority=沒有為用戶{0}指定角色
JdbcDaoImpl.notFound=未找到用戶{0}
LdapAuthenticationProvider.badCredentials=壞的憑證
LdapAuthenticationProvider.credentialsExpired=用戶憑證已過期
LdapAuthenticationProvider.disabled=用戶已失效
LdapAuthenticationProvider.expired=用戶帳號已過期
LdapAuthenticationProvider.locked=用戶帳號已被鎖定
LdapAuthenticationProvider.emptyUsername=用戶名不允許為空
LdapAuthenticationProvider.onlySupports=僅僅支持UsernamePasswordAuthenticationToken
PasswordComparisonAuthenticator.badCredentials=壞的憑證
#PersistentTokenBasedRememberMeServices.cookieStolen=Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.
ProviderManager.providerNotFound=未查找到針對{0}的AuthenticationProvider
RememberMeAuthenticationProvider.incorrectKey=展示RememberMeAuthenticationToken不含有預期的key
RunAsImplAuthenticationProvider.incorrectKey=展示的RunAsUserToken不含有預期的key
SubjectDnX509PrincipalExtractor.noMatching=未在subjectDN\: {0}中找到匹配的模式
SwitchUserFilter.noCurrentUser=不存在當前用戶
SwitchUserFilter.noOriginalAuthentication=不能夠查找到原先的已認證對象
Spring Security 通過 org.springframework.security.core.SpringSecurityMessageSource
類讀取該組配置文件,將其作為 MessageSource
使用:
/**
* The default <code>MessageSource</code> used by Spring Security.
* <p>
* All Spring Security classes requiring message localization will by default use this
* class. However, all such classes will also implement <code>MessageSourceAware</code> so
* that the application context can inject an alternative message source. Therefore this
* class is only used when the deployment environment has not specified an alternative
* message source.
* </p>
*
* @author Ben Alex
*/
public class SpringSecurityMessageSource extends ResourceBundleMessageSource {
public SpringSecurityMessageSource() {
setBasename("org.springframework.security.messages");
}
public static MessageSourceAccessor getAccessor() {
return new MessageSourceAccessor(new SpringSecurityMessageSource());
}
}
Spring Security 的很多類都會調(diào)用 SpringSecurityMessageSource.getAccessor()
方法來獲取 MessageSourceAccessor
的對象蜘醋,進而使用 getMessage(...)
方法訪問前述的配置文件信息:
/**
* Helper class for easy access to messages from a MessageSource,
* providing various overloaded getMessage methods.
*
* <p>Available from ApplicationObjectSupport, but also reusable
* as a standalone helper to delegate to in application objects.
*
* @author Juergen Hoeller
* @since 23.10.2003
* @see ApplicationObjectSupport#getMessageSourceAccessor
*/
public class MessageSourceAccessor {
private final MessageSource messageSource;
@Nullable
private final Locale defaultLocale;
/**
* Create a new MessageSourceAccessor, using LocaleContextHolder's locale
* as default locale.
* @param messageSource the MessageSource to wrap
* @see org.springframework.context.i18n.LocaleContextHolder#getLocale()
*/
public MessageSourceAccessor(MessageSource messageSource) {
this.messageSource = messageSource;
this.defaultLocale = null;
}
/**
* Retrieve the message for the given code and the default Locale.
* @param code code of the message
* @param defaultMessage String to return if the lookup fails
* @return the message
*/
public String getMessage(String code, String defaultMessage) {
String msg = this.messageSource.getMessage(code, null, defaultMessage, getDefaultLocale());
return (msg != null ? msg : "");
}
//……
}
踩坑
SpringSecurityMessageSource
類的注釋是這樣說的:
The default
MessageSource
used by Spring Security.
All Spring Security classes requiring message localization will by default use this class. However, all such classes will also implementMessageSourceAware
so that the application context can inject an alternative message source. Therefore this class is only used when the deployment environment has not specified an alternative message source.
Spring Security 默認使用的
MessageSource
胁塞。
所有需要本地化信息的 Spring Security 類默認使用該類。不過,這些類也都實現(xiàn)了MessageSourceAware
啸罢,以便應(yīng)用程序上下文可以注入一個可供替代的信息源(message source)编检。因此這個類只在部署環(huán)境沒有另一個信息源時使用。
再結(jié)合官方手冊的說明 https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#localization扰才,我們知道允懂,可以提供一個名為 messageSource
的 bean,該 bean 通過 MessageSourceAware
接口自動注入到 Spring Security 的類:
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:messages"/>
</bean>
接下來只需要拷貝一份 messages_zh_CN.properties
配置文件到我們的 classpath
即 Maven 目錄結(jié)構(gòu)的 resources
下衩匣,修改錯誤提示信息即可蕾总。
然而上述配置在我的 Spring Boot 2.0.4.RELEASE 集成環(huán)境下并沒有起到預期的效果!@拍蟆生百!
在代碼中注入上述配置的 messageSource
bean,調(diào)用 getMessage(...)
方法確實能拿到我們自定義的錯誤提示信息柄延,但是 Spring Security 返回給頁面的提示仍然是壞的憑證
……
解決方案
拷貝一份 messages_zh_CN.properties
配置文件到我們的 classpath
即 Maven 目錄結(jié)構(gòu)的 resources
下的 org/springframework/security
目錄中蚀浆,修改錯誤提示信息即可。
不需要配置 messageSource
的 bean搜吧,此方案直接暴力覆蓋了 Spring Security 的配置文件市俊,Spring Security 通過自己的默認配置加載的就是我們的 messages[_language_country].properties
。
題外話
百度 + 谷歌 + 官方手冊 + Spring Security 源碼赎败,此問題耗費了我兩個多小時的寶貴時間秕衙,而且我隱約記得之前也解決過這個問題。
好記性不如爛筆頭僵刮,特此記下据忘,利人利己,何樂而不為呢搞糕。