① 身份认证
身份认证是第一道门户,进去之后才能谈论授权的问题。
身份验证,一般需要提供如身份ID 等一些标识信息来表明登录者的身份,如提供email,用户名/密码来证明。
在shiro中,用户需要提供principals (身份)和credentials(证明)给shiro,从而应用能验证用户身份:
最常见的principals 和credentials 组合就是用户名/密码了。
② 身份认证流程
1)收集用户身份/凭证,即如用户名/密码;
2)调用Subject.login进行登录,如果失败将得到相应的AuthenticationException异常,根据异常提示用户错误信息;否则登录成功;
第2)步细节如下:
如下图所示:
如下示例:
③ 认证过程详解
步骤如下:
首先调用Subject.login(token) 进行登录,其会自动委托给SecurityManager
SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator 进行身份验证;
Authenticator 才是真正的身份验证者,ShiroAPI 中核心的身份认证入口点,此处可以自定义插入自己的实现;
Authenticator 可能会委托给相应的AuthenticationStrategy进行多Realm 身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm 身份验证;
Authenticator 会把相应的token 传入Realm(通常是你的自定义CustomRealm.doGetAuthenticationInfo()),从Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
密码的比对:通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!
④ AuthenticationException
如果身份验证失败请捕获AuthenticationException或其子类。最好使用如“用户名/密码错误”而不是“用户名错误”/“密码错误”,防止一些恶意用户非法扫描帐号库。
⑤ Realm
Shiro从Realm 获取安全数据(如用户、角色、权限),即SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
Realm接口如下:
String getName();//返回一个唯一的Realm名字
boolean supports(AuthenticationToken token);//判断此reaml是否支持此Token
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException;//根据Token获取认证信息
一般继承AuthorizingRealm(授权)即可;其继承了AuthenticatingRealm(即身份验证),而且也间接继承了CachingRealm(带有缓存实现)。
Realm 的继承关系如下图示:
⑥ Authenticator
Authenticator 的职责是验证用户帐号,是ShiroAPI 中身份验证核心的入口点。如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException异常。
SecurityManager接口继承了Authenticator接口,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm 进行验证,验证规则通过AuthenticationStrategy接口指定。
⑦ AuthenticationStrategy
AuthenticationStrategy接口的默认实现:
如下图所示:
ModularRealmAuthenticator默认是AtLeastOneSuccessfulStrategy策略。看不懂没关系,继续往下看。
⑦ 代码示例
登录方法如下:
@RequestMapping(value="/doLogin" )
public String doLogin(Model model, HttpSession session, HttpServletRequest request, HttpServletResponse response, String userName, String password, String randomCode) throws Exception{
String msg;
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
log.debug("UserNamePasswordToken----:"+JSON.toJSONString(token));
token.setRememberMe(true);
request.setAttribute("userName", userName);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
if (subject.isAuthenticated()) {
return "redirect:/index";
}
} catch (UserNameException e) {
msg = e.getMessage();//Password for account " + token.getPrincipal() + " was incorrect.
model.addAttribute("message", msg);
} catch (PasswordException e) {
msg = e.getMessage();//Password for account " + token.getPrincipal() + " was incorrect.
model.addAttribute("message", msg);
} catch (IncorrectCredentialsException e) {
msg = "登录密码错误. ";//Password for account " + token.getPrincipal() + " was incorrect.
model.addAttribute("message", msg);
} catch (ExcessiveAttemptsException e) {
msg = "登录失败次数过多.";
model.addAttribute("message", msg);
} catch (LockedAccountException e) {
msg = "帐号已被锁定,如有疑问请联系管理员. ";//The account for username " + token.getPrincipal() + " was locked.
model.addAttribute("message", msg);
} catch (DisabledAccountException e) {
msg = "帐号已被禁用. ";//The account for username " + token.getPrincipal() + " was disabled.
model.addAttribute("message", msg);
} catch (ExpiredCredentialsException e) {
msg = "帐号已过期. ";//the account for username " + token.getPrincipal() + " was expired.
model.addAttribute("message", msg);
} catch (UnknownAccountException e) {
msg = "帐号不存在. ";//There is no user with username of " + token.getPrincipal()
model.addAttribute("message", msg);
} catch (UnauthorizedException e) {
msg = "您没有得到相应的授权!" + e.getMessage();
model.addAttribute("message", msg);
}
return "forward:/login";
}
自定义CustomRealm.doGetAuthenticationInfo()方法如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取页面传来的用户账号
String loginName = token.getUsername();
//根据登录账号从数据库查询用户信息
SysUser user = sysUserService.getUserByLoginCode(loginName);
System.out.println("从数据库查询到的用户信息 : "+user);
//一些异常新娘西
if (null == user) {
throw new UnknownAccountException();//没找到帐号
}
if (user.getStatus()==null||user.getStatus()==0) {
throw new LockedAccountException();//帐号被锁定
}
//其他异常...
//返回AuthenticationInfo的实现类SimpleAuthenticationInfo
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
}
需要注意的是在登录成功后,注销登录一定要走Shiro过滤器 logout:
/logout=logout
或者自定义注销过滤器,总之要完成如下的功能:任何现有的Session都将会失效,而且任何身份都将会失去关联。在web应用程序中,RememberMe cookie也将被删除。
否则,虽然注销成功了,Shiro缓存中还认为该用户认证成功,用户访问需要认证的连接时,将会直接通过!
多Realm认证需要将ModularRealmAuthenticator作为securityManager属性注册到容器中。
① 如下配置两个自定义Realm:
② 将两个自定义Realm配置为bean-authenticator-class为ModularRealmAuthenticator的属性
③ 将配置的authenticator注入到securityManager中
④ 多Realm下就需要考虑上面说的认证策略了
在authenticator中配置如下:
上面是把realms配置给authenticator,当然也可以直接把realms配置给securityManager。
securityManager此时配置如下所示:
authenticator配置此时如下所示:
其他同上。