1.shiro概述:
shiro是一个功能强大且易于使用的Java安全框架,它的认证,授权,加密和会话管理可以用于保护任何应用程序——来自从命令行应用程序、移动应用程序到最大的web和企业应用程序。
shiro为以下几个方面提供应用程序的安全API(应用程序安全的4大基石):
Authentication - 提供用户身份认证,俗称登录
Authorization - 访问权限控制
Cryptography - 使用加密算法保护或者隐藏数据
Session Management - 用户的会话管理
login方法调用:
/**
* 登录
*/
@ApiOperation("用户登录")
@ResponseBody
@PostMapping(value = "/sys/login")
public R login(String username, String password, String captcha) {
String kaptcha = ShiroUtils.getKaptcha ( Constants.KAPTCHA_SESSION_KEY );
if (!captcha.equalsIgnoreCase ( kaptcha )) {
return R.error ( "验证码不正确" );
}
try {
/* ShiroUtils其实就是自定义对SecurityUtils做进一步优化 */
/* 获取项目管理信息 */
Subject subject = ShiroUtils.getSubject ();
UsernamePasswordToken token = new UsernamePasswordToken ( username, password );
subject.login ( token );
} catch (UnknownAccountException e) {
return R.error ( e.getMessage () );
} catch (IncorrectCredentialsException e) {
return R.error ( "账号或密码不正确" );
} catch (LockedAccountException e) {
return R.error ( "账号已被锁定,请联系管理员" );
} catch (AuthenticationException e) {
return R.error ( "账户验证失败" );
}
return R.ok ();
}
2.shiro 认证机制
这个过程主要分为三个部分:
1:获取客户端输入放入用户名,密码。
2:获取数据源中存放的数据即相应的用户名,密码。
3:进行两者的比对,判断是否登录操作成功。
先看一下shiro中是如何实现这个认证的过程的吧:
image.png通过当前的用户对象Subject执行login()方法将用户信息传给Shiro的SecurityManager,而这个SecurityManager会将用户信息委托给内部登录模块,由内部登录模块来调用Realm中的方法来进行数据比对进而判断是否登录成功
3. Subject,SecurityManager,Realm
3.1 Subject
概述:官方文档
Subject
When you’re securing your application, probably the most relevant questions to ask yourself are, “Who is the current user?” or “Is the current user allowed to do X”? It is common for us to ask ourselves these questions as we're writing code or designing user interfaces: applications are usually built based on user stories, and you want functionality represented (and secured) based on a per-user basis. So, the most natural way for us to think about security in our application is based on the current user. Shiro’s API fundamentally represents this way of thinking in its Subject concept.
The word Subject is a security term that basically means "the currently executing user". It's just not called a 'User' because the word 'User' is usually associated with a human being. In the security world, the term 'Subject' can mean a human being, but also a 3rd party process, daemon account, or anything similar. It simply means 'the thing that is currently interacting with the software'. For most intents and purposes though, you can think of this as Shiro’s ‘User’ concept. You can easily acquire the Shiro Subject anywhere in your code as shown in Listing 1 below.
大意就是:subject指的是当前用户,因为我们人的思维更倾向于某个用户有某个角色,因此可以理解为基于当前用户。(不过在安全领域,术语“Subject”可以指一个人,也可以指第三方进程、守护进程帐户或任何类似的东西。)
它的获取方法在官方文档中定义为:相信在我上面的部分项目代码中大家也已经看到了,这块用法是固定的:
通过SecurityUtils工具类直接调用:
Subject currentUser = SecurityUtils.getSubject();
一旦您获得了subject,就可以立即访问当前用户使用Shiro想要做的90%的事情,比如登录、注销、访问他们的会话、执行授权检查,等等.
3.2 SecurityManager
SecurityManager是shiro架构核心,协调内部安全组件(如登录,授权,数据源等),用来管理所有的subject。 它负责安全认证与授权。Shiro本身已经实现了所有的细节,用户可以完全把它当做一个黑盒来使用。SecurityUtils对象,本质上就是一个工厂类似Spring中的ApplicationContext。Subject是初学者比较难于理解的对象,很多人以为它可以等同于User,其实不然。Subject中文翻译:项目,而正确的理解也恰恰如此。它是你目前所设计的需要通过Shiro保护的项目的一个抽象概念。通过令牌(token)与项目(subject)的登陆(login)关系,Shiro保证了项目整体的安全。
3.3 Realm
概述:官方文档定义:
Realms
The third and final core concept in Shiro is that of a Realm. A Realm acts as the ‘bridge’ or ‘connector’ between Shiro and your application’s security data. That is, when it comes time to actually interact with security-related data like user accounts to perform authentication (login) and authorization (access control), Shiro looks up many of these things from one or more Realms configured for an application.
In this sense a Realm is essentially a security-specific DAO: it encapsulates connection details for data sources and makes the associated data available to Shiro as needed. When configuring Shiro, you must specify at least one Realm to use for authentication and/or authorization. More than one Realm may be configured, but at least one is required.
Shiro provides out-of-the-box Realms to connect to a number of security data sources (aka directories) such as LDAP, relational databases (JDBC), text configuration sources like INI and properties files, and more. You can plug-in your own Realm implementations to represent custom data sources if the default Realms do not meet your needs. Listing 4 below is an example of configuring Shiro (via INI) to use an LDAP directory as one of the application’s Realms.
大意是指:Realm充当的是Shiro和应用程序安全数据之间的“桥梁”或“连接器”。也就是说,当实际需要与与安全相关的数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从一个或多个为应用程序配置的Realm中查找这些内容。也是说Realm本质上是一个特定于安全性的DAO(逻辑处理):它封装了数据源的连接细节,并根据需要将关联的数据提供给Shiro。在配置Shiro时,必须指定至少一个用于身份验证和/或授权的领域。可以配置多个域,但至少需要一个。
Shiro提供了开箱即用的领域,可以连接到许多安全数据源(即目录),如LDAP、关系数据库(JDBC)、文本配置源(如INI)和属性文件,等等。也可以自定义Realm实现来表示自定义数据源。
shiro结构图:
image.png获取Subject:
/* ShiroUtils其实就是自定义对SecurityUtils做进一步优化 */
/* 获取项目管理信息 */
Subject subject = ShiroUtils.getSubject ();
调用login方法:
UsernamePasswordToken token = new UsernamePasswordToken ( username, password );
subject.login ( token );
内部调用的是subject接口声明的方法:
void login(AuthenticationToken token) throws AuthenticationException;
login()实现源码:
实现类:
public class DelegatingSubject implements Subject {
实现方法:
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}
其内部调用的方法是:
Subject subject = securityManager.login(this, token);
对securityManager.login(this,token);源码进行跟踪:
Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException;
实现类:
public class DefaultSecurityManager extends SessionsSecurityManager {
实现方法:
/**
* First authenticates the {@code AuthenticationToken} argument, and if successful, constructs a
* {@code Subject} instance representing the authenticated account's identity.
* <p/>
* Once constructed, the {@code Subject} instance is then {@link #bind bound} to the application for
* subsequent access before being returned to the caller.
*
* @param token the authenticationToken to process for the login attempt.
* @return a Subject representing the authenticated user.
* @throws AuthenticationException if there is a problem authenticating the specified {@code token}.
*/
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
其中内部调用了 AuthenticationInfo info = authenticate(token);
SecurityManager接口中它所继承的类:
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
从这里我们可以发现SecurityManager继承了 登录认证的接口比如登录(Authenticator),权限验证(Authorizer)等。
其中Authenticator中定义了 authenticate认证方法:
public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}
即DefaultSecurityManager接口中对login实现调用的方法:
/**
* First authenticates the {@code AuthenticationToken} argument, and if successful, constructs a
* {@code Subject} instance representing the authenticated account's identity.
* <p/>
* Once constructed, the {@code Subject} instance is then {@link #bind bound} to the application for
* subsequent access before being returned to the caller.
*
* @param token the authenticationToken to process for the login attempt.
* @return a Subject representing the authenticated user.
* @throws AuthenticationException if there is a problem authenticating the specified {@code token}.
*/
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
try {
onFailedLogin(token, ae, subject);
} catch (Exception e) {
if (log.isInfoEnabled()) {
log.info("onFailedLogin method threw an " +
"exception. Logging and propagating original AuthenticationException.", e);
}
}
throw ae; //propagate
}
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
AbstractAuthenticator中authenticate()的实现:
/**
* Implementation of the {@link Authenticator} interface that functions in the following manner:
* <ol>
* <li>Calls template {@link #doAuthenticate doAuthenticate} method for subclass execution of the actual
* authentication behavior.</li>
* <li>If an {@code AuthenticationException} is thrown during {@code doAuthenticate},
* {@link #notifyFailure(AuthenticationToken, AuthenticationException) notify} any registered
* {@link AuthenticationListener AuthenticationListener}s of the exception and then propagate the exception
* for the caller to handle.</li>
* <li>If no exception is thrown (indicating a successful login),
* {@link #notifySuccess(AuthenticationToken, AuthenticationInfo) notify} any registered
* {@link AuthenticationListener AuthenticationListener}s of the successful attempt.</li>
* <li>Return the {@code AuthenticationInfo}</li>
* </ol>
*
* @param token the submitted token representing the subject's (user's) login principals and credentials.
* @return the AuthenticationInfo referencing the authenticated user's account data.
* @throws AuthenticationException if there is any problem during the authentication process - see the
* interface's JavaDoc for a more detailed explanation.
*/
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
}
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this " +
"Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
//Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more
//severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate:
String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " +
"error? (Typical or expected login exceptions should extend from AuthenticationException).";
ae = new AuthenticationException(msg, t);
if (log.isWarnEnabled())
log.warn(msg, t);
}
try {
notifyFailure(token, ae);
} catch (Throwable t2) {
if (log.isWarnEnabled()) {
String msg = "Unable to send notification for failed authentication attempt - listener error?. " +
"Please check your AuthenticationListener implementation(s). Logging sending exception " +
"and propagating original AuthenticationException instead...";
log.warn(msg, t2);
}
}
throw ae;
}
log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
notifySuccess(token, info);
return info;
}
调用了doAuthenticate(token)方法
/**
* Template design pattern hook for subclasses to implement specific authentication behavior.
* <p/>
* Common behavior for most authentication attempts is encapsulated in the
* {@link #authenticate} method and that method invokes this one for custom behavior.
* <p/>
* <b>N.B.</b> Subclasses <em>should</em> throw some kind of
* {@code AuthenticationException} if there is a problem during
* authentication instead of returning {@code null}. A {@code null} return value indicates
* a configuration or programming error, since {@code AuthenticationException}s should
* indicate any expected problem (such as an unknown account or username, or invalid password, etc).
*
* @param token the authentication token encapsulating the user's login information.
* @return an {@code AuthenticationInfo} object encapsulating the user's account information
* important to Shiro.
* @throws AuthenticationException if there is a problem logging in the user.
*/
protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token)
throws AuthenticationException;
我们再来看ModularRealmAuthenticator中doAuthenticate(token)方法的实现:
/**
* Attempts to authenticate the given token by iterating over the internal collection of
* {@link Realm}s. For each realm, first the {@link Realm#supports(org.apache.shiro.authc.AuthenticationToken)}
* method will be called to determine if the realm supports the {@code authenticationToken} method argument.
* <p/>
* If a realm does support
* the token, its {@link Realm#getAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)}
* method will be called. If the realm returns a non-null account, the token will be
* considered authenticated for that realm and the account data recorded. If the realm returns {@code null},
* the next realm will be consulted. If no realms support the token or all supporting realms return null,
* an {@link AuthenticationException} will be thrown to indicate that the user could not be authenticated.
* <p/>
* After all realms have been consulted, the information from each realm is aggregated into a single
* {@link AuthenticationInfo} object and returned.
*
* @param authenticationToken the token containing the authentication principal and credentials for the
* user being authenticated.
* @return account information attributed to the authenticated user.
* @throws IllegalStateException if no realms have been configured at the time this method is invoked
* @throws AuthenticationException if the user could not be authenticated or the user is denied authentication
* for the given principal and credentials.
*/
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
调用了doSingleRealmAuthentication(realms.iterator().next(), authenticationToken)
再往下看:doSingleRealmAuthentication的实现:
/**
* Performs the authentication attempt by interacting with the single configured realm, which is significantly
* simpler than performing multi-realm logic.
*
* @param realm the realm to consult for AuthenticationInfo.
* @param token the submitted AuthenticationToken representing the subject's (user's) log-in principals and credentials.
* @return the AuthenticationInfo associated with the user account corresponding to the specified {@code token}
*/
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
它调用Realm的getAuthenticationInfo(token)方法
而在Realm中我们看一下用户认证方法重写:
/**
*
* ClassName:com.code.modules.sys.shiro.UserRealm <br>
* Description:(shiro认证)<br>
* @author wangxiong <br>
* date 2020/4/8 15:21<br>
* @version v1.0 <br>
*/
@Component
public class UserRealm extends AuthorizingRealm {
@Autowired
private SysUserDao sysUserDao;
@Autowired
private SysMenuDao sysMenuDao;
/**
* 授权(验证权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal ();
Long userId = user.getUserId ();
List<String> permsList;
//系统管理员,拥有最高权限
if (userId == Constant.SUPER_ADMIN) {
/* 查询所有菜单 */
List<SysMenuEntity> menuList = sysMenuDao.selectList ( null );
permsList = new ArrayList<> ( menuList.size () );
for (SysMenuEntity menu : menuList) {
/* 添加权限 */
permsList.add ( menu.getPerms () );
}
} else {
permsList = sysUserDao.queryAllPerms ( userId ); //关联查询 user - role - menu
}
//用户权限列表
Set<String> permsSet = new HashSet<> ();
for (String perms : permsList) {
if (StringUtils.isBlank ( perms )) {
continue;
}
/* 查询以逗号分隔的所有授权信息 */
permsSet.addAll ( Arrays.asList ( perms.trim ().split ( "," ) ) );
}
/* 授权 */
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo ();
info.setStringPermissions ( permsSet );
return info;
}
/**
* 认证(登录时调用)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authcToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
//查询用户信息
SysUserEntity user = sysUserDao.selectOne ( new QueryWrapper<SysUserEntity> ().eq ( "username", token.getUsername () ) );
//账号不存在
if (user == null) {
throw new UnknownAccountException ( ExceptionEnum.ACCOUNT_OR_PASSWORD_IS_INCORRECT.getMsg () );
}
//账号锁定
if (user.getStatus () == 0) {
throw new LockedAccountException ( ExceptionEnum.ACCOUNT_IS_LOCKED.getMsg () );
}
//授权
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo ( user, user.getPassword (), ByteSource.Util.bytes ( user.getSalt () ), getName () );
return info;
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher ();
shaCredentialsMatcher.setHashAlgorithmName ( ShiroUtils.hashAlgorithmName );
shaCredentialsMatcher.setHashIterations ( ShiroUtils.hashIterations );
super.setCredentialsMatcher ( shaCredentialsMatcher );
}
}
主要重写了俩个方法:
doGetAuthenticationInfo()主要是进行登录认证
doGetAuthorizationInfo()主要是进行角色权限和对应权限的添加
Shiro 配置
要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现(类似SpringMvc 通过DispachServlet 来主控制一样)
filter主要是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。如下:
/**
*
* ClassName:com.code.common.config.ShiroConfig <br>
* Description:(Shiro的配置文件)<br>
* @author wangxiong <br>
* date 2020/4/8 14:25<br>
* @version v1.0 <br>
*/
@Configuration
public class ShiroConfig {
/**
* 单机环境,session交给shiro管理
*/
@Bean
@ConditionalOnProperty(prefix = "springboot_template", name = "cluster", havingValue = "false")
public DefaultWebSessionManager sessionManager(@Value("${springboot_template.globalSessionTimeout:3600}") long globalSessionTimeout) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager ();
/* 开启session会话任务调度效验 */
sessionManager.setSessionValidationSchedulerEnabled ( true );
/* 去掉shiro登录时url里的JSESSIONID */
sessionManager.setSessionIdUrlRewritingEnabled ( false );
/* 会话验证间隔 */
sessionManager.setSessionValidationInterval ( globalSessionTimeout * 1000 );
/* 会话超时 */
sessionManager.setGlobalSessionTimeout ( globalSessionTimeout * 1000 );
return sessionManager;
}
/**
* 集群环境,session交给spring-session管理
*/
@Bean
@ConditionalOnProperty(prefix = "springboot_template", name = "cluster", havingValue = "true")
public ServletContainerSessionManager servletContainerSessionManager() {
return new ServletContainerSessionManager ();
}
//权限管理,配置主要是Realm的管理认证
@Bean("securityManager")
public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager ();
securityManager.setRealm ( userRealm );
securityManager.setSessionManager ( sessionManager );
securityManager.setRememberMeManager ( null );
return securityManager;
}
/**
* Title: shiroFilter<br>
* Author: Man<br>
* Description: (权限认证过滤)<br>
* Date: 10:14 <br>
*
* @param securityManager return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean ();
shiroFilter.setSecurityManager ( securityManager );
shiroFilter.setLoginUrl ( "/login.html" );
shiroFilter.setUnauthorizedUrl ( "/" );
Map<String, String> filterMap = new LinkedHashMap<> ();
/* anon 不做验证;authc 要做验证 */
filterMap.put ( "/swagger/**", "anon" );
filterMap.put ( "/v2/api-docs", "anon" );
filterMap.put ( "/swagger-ui.html", "anon" );
filterMap.put ( "/webjars/**", "anon" );
filterMap.put ( "/swagger-resources/**", "anon" );
filterMap.put ( "/statics/**", "anon" );
filterMap.put ( "/login.html", "anon" );
filterMap.put ( "/sys/login", "anon" );
filterMap.put ( "/favicon.ico", "anon" );
filterMap.put ( "/captcha.jpg", "anon" );
filterMap.put ( "/**", "authc" );
shiroFilter.setFilterChainDefinitionMap ( filterMap );
return shiroFilter;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor ();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor ();
advisor.setSecurityManager ( securityManager );
return advisor;
}
}
方法一与方法二是配置单机与集群管理。
方法三: 权限管理,配置主要是Realm的管理认证。
方法四: Filter工厂,设置对应的过滤条件和跳转条件。
异常捕获
在登录过程中可能会出现不同的异常,对于不同的异常,我们是如何处理的呢?
当然不同的异常就要分类进行处理,比如密码错误和账户不存在就不能一概而论,对于这些问题,我们能做的就是将不同的异常进行捕获进行不同页面的跳转反馈给用户,提高用户体验,比如:
try {
/* ShiroUtils其实就是自定义对SecurityUtils做进一步优化 */
/* 获取项目管理信息 */
Subject subject = ShiroUtils.getSubject ();
UsernamePasswordToken token = new UsernamePasswordToken ( username, password );
subject.login ( token );
} catch (UnknownAccountException e) {
return R.error ( e.getMessage () );
} catch (IncorrectCredentialsException e) {
return R.error ( "账号或密码不正确" );
} catch (LockedAccountException e) {
return R.error ( "账号已被锁定,请联系管理员" );
} catch (AuthenticationException e) {
return R.error ( "账户验证失败" );
}
网友评论