定制 Spring Security 错误提示信息

使用过 Spring Security 框架的同学应该都见过这个提示:坏的凭证,对应的英文提示为 Bad credentials。不知道是不是机翻,反正我看着十分别扭。客户拿到你做的系统,输入错误的用户名或密码时,看到坏的凭证这样的提示肯定是一头雾水。

下面就来教大家如何定制 Spring Security 的错误提示信息。

分析

spring-security-core-5.0.7.RELEASE.jarorg.springframework.security 包下,有一组名为 messages[_language_country].properties 的配置文件。

messages_zh_CN.properties 内容如下:

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})根本无权操控领域对象({1})
AnonymousAuthenticationProvider.incorrectKey=展示的AnonymousAuthenticationToken不含有预期的key
BindAuthenticator.badCredentials=坏的凭证
BindAuthenticator.emptyPassword=坏的凭证
CasAuthenticationProvider.incorrectKey=展示的CasAuthenticationToken不含有预期的key
CasAuthenticationProvider.noServiceTicket=未能够正确提供待验证的CAS服务票根
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=已经超过了当前主体({0})被允许的最大会话数量
DigestAuthenticationFilter.incorrectRealm=响应结果中的Realm名字({0})同系统指定的Realm名字({1})不吻合
DigestAuthenticationFilter.incorrectResponse=错误的响应结果
DigestAuthenticationFilter.missingAuth=遗漏了针对'auth' QOP的、必须给定的摘要取值; 接收到的头信息为{0}
DigestAuthenticationFilter.missingMandatory=遗漏了必须给定的摘要取值; 接收到的头信息为{0}
DigestAuthenticationFilter.nonceCompromised=Nonce令牌已经存在问题了,{0}
DigestAuthenticationFilter.nonceEncoding=Nonce未经过Base64编码; 相应的nonce取值为 {0}
DigestAuthenticationFilter.nonceExpired=Nonce已经过期/超时
DigestAuthenticationFilter.nonceNotNumeric=Nonce令牌的第1部分应该是数字,但结果却是{0}
DigestAuthenticationFilter.nonceNotTwoTokens=Nonce应该由两部分取值构成,但结果却是{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 的很多类都会调用 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 implement MessageSourceAware 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 类默认使用该类。不过,这些类也都实现了 MessageSourceAware,以便应用程序上下文可以注入一个可供替代的信息源(message source)。因此这个类只在部署环境没有另一个信息源时使用。

再结合官方手册的说明 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 目录结构的 resources 下,修改错误提示信息即可。

然而上述配置在我的 Spring Boot 2.0.4.RELEASE 集成环境下并没有起到预期的效果!!!

在代码中注入上述配置的 messageSource bean,调用 getMessage(...) 方法确实能拿到我们自定义的错误提示信息,但是 Spring Security 返回给页面的提示仍然是坏的凭证……

解决方案

拷贝一份 messages_zh_CN.properties 配置文件到我们的 classpath 即 Maven 目录结构的 resources 下的 org/springframework/security 目录中,修改错误提示信息即可。

不需要配置 messageSource 的 bean,此方案直接暴力覆盖了 Spring Security 的配置文件,Spring Security 通过自己的默认配置加载的就是我们的 messages[_language_country].properties

题外话

百度 + 谷歌 + 官方手册 + Spring Security 源码,此问题耗费了我两个多小时的宝贵时间,而且我隐约记得之前也解决过这个问题。

好记性不如烂笔头,特此记下,利人利己,何乐而不为呢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容