即安全数据源
一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),
而且也间接继承了 CachingRealm(带有缓存实现)。
权限系统实体类定义
用户实体包括:编号(id)、用户名(username)、密码(password)、盐(salt)、是否锁定(locked);
是否锁定用于封禁用户使用,其实最好使用 Enum 字段存储,可以实现更复杂的用户状态实现。
package com.cxy.pojo;
public class User {
private Long id;
private String name;
private String password;
private String salt;
//private String isLocked;是否锁定
private UserStateEnum state = UserStateEnum.NORMAL;//默认正常状态
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password == null ? null : password.trim();
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt == null ? null : salt.trim();
}
}
角色实体包括:编号(id)、角色标识符(role)、描述(description)、是否可用(available);
其中角色标识符用于在程序中进行隐式角色判断的,描述用于以后再前台界面显示的、是否可用表示角色当前是否激活。
package com.cxy.pojo;
public class Role {
private Long id;
private String name;
private String desc_;//描述
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getDesc_() {
return desc_;
}
public void setDesc_(String desc_) {
this.desc_ = desc_ == null ? null : desc_.trim();
}
}
权限实体包括:编号(id)、权限标识符(permission)、描述(description)、对应的url(url),是否可用(available);
package com.cxy.pojo;
public class Permission {
private Long id;
private String name;
private String desc_;
private String url;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name == null ? null : name.trim();
}
public String getDesc_() {
return desc_;
}
public void setDesc_(String desc_) {
this.desc_ = desc_ == null ? null : desc_.trim();
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url == null ? null : url.trim();
}
}
另外还有两个关系实体:
用户-角色实体(id,用户id、角色id);
package com.cxy.pojo;
public class UserRole {
private Long id;
private Long uid;
private Long rid;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public Long getRid() {
return rid;
}
public void setRid(Long rid) {
this.rid = rid;
}
}
角色-权限实体(id,角色id、权限id)
package com.cxy.pojo;
public class RolePermission {
private Long id;
private Long rid;
private Long pid;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getRid() {
return rid;
}
public void setRid(Long rid) {
this.rid = rid;
}
public Long getPid() {
return pid;
}
public void setPid(Long pid) {
this.pid = pid;
}
定义Realm
1、继承的父类 AuthorizingRealm 将获取 Subject 相关信息分成两步:获取身份验证信息(doGetAuthenticationInfo)及授权信息(doGetAuthorizationInfo);
2、doGetAuthenticationInfo 获取身份验证相关信息:首先根据传入的用户名获取 User 信
息;然后如果 user 为空,那么抛出没找到帐号异常 UnknownAccountException;如果 user
找到但锁定了抛出锁定异常 LockedAccountException;最后生成 AuthenticationInfo 信息,
交给间接父类 AuthenticatingRealm 使用 CredentialsMatcher 进行判断密码是否匹配,如果不
匹配将抛出密码错误异常 IncorrectCredentialsException;另外如果密码重试此处太多将抛出
超出重试次数异常 ExcessiveAttemptsException;
当然,实际中如果无特殊要求,直接抛出AuthenticationException,不给坏人机会
在组装 SimpleAuthenticationInfo 信息时,
需要传入:身份信息(用户名)、凭据(密文密码)、盐(salt),CredentialsMatcher
使用盐加密传入的明文密码和此处的密文密码进行匹配。
3、doGetAuthorizationInfo 获取授权信息:PrincipalCollection 是一个身份集合,因为我们
现在就一个 Realm,所以直接调用 getPrimaryPrincipal 得到之前传入的用户名即可;然后根
据用户名调用 UserService 接口获取角色及权限信息。
package com.cxy.realm;
import java.util.Set;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import com.cxy.service.PermissionService;
import com.cxy.service.RoleService;
import com.cxy.service.UserService;
import md5.Md5Test;
/**
* 安全认证实现类,获取Subject
* @author cxy
*
*/
public class DatabaseRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
//获取授权信息
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//能进入到这里,表示账号已经通过验证了
String userName =(String) principalCollection.getPrimaryPrincipal();
//通过service获取角色和权限
Set<String> permissions = permissionService.listPermissions(userName);
Set<String> roles = roleService.listRoleNames(userName);
//创建授权对象
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
//把通过service获取到的角色和权限放进去
s.setStringPermissions(permissions);
s.setRoles(roles);
return s;
}
//获取身份验证信息
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String userName= token.getPrincipal().toString();
//获取数据库中的密码
String passwordInDB = userService.getPassword(userName);
String salt = userService.getByName(userName).getSalt();
//如果为空就是账号不存在,如果不相同就是密码错误,但是都抛出AuthenticationException,而不是抛出具体错误原因,免得给破解者提供帮助信息
if(null==passwordInDB )
{
throw new AuthenticationException();
}
//认证信息里存放账号密码,
//用户名是AuthenticationToken传过来的用户名
//密码是数据库查询的密码
//getName() 是当前Realm的继承方法,通常返回当前类名 :databaseRealm
//交由spring-context-shiro.xml的HashedCredentialsMatcher校验
SimpleAuthenticationInfo a = new SimpleAuthenticationInfo(userName,passwordInDB,ByteSource.Util.bytes(salt),getName());
return a;
}
}
关于 AuthenticationToken
包含了身份(如用户名)及凭据(如密码):
public interface AuthenticationToken extends Serializable {
Object getPrincipal(); //身份
Object getCredentials(); //凭据
}
扩展接口 RememberMeAuthenticationToken:提供了“boolean isRememberMe() 记住我的功能
扩展接口是 HostAuthenticationToken:提供了“String getHost()”方法用于获取用户“主机”的功能
Shiro 提供了一个直接拿来用的 UsernamePasswordToken,用于实现用户名/密码 Token 组,
另外其实现了 RememberMeAuthenticationToken 和 HostAuthenticationToken,可以实现记住
我及主机验证的支持,只要进行 UsernamePasswordToken t = ( UsernamePasswordToken )token ,转换下就行了
AuthenticationInfo
1、如果 定义的xxxRealm 继承了 AuthenticatingRealm ,则提供给 AuthenticatingRealm 内部使用的
CredentialsMatcher 进行凭据验证;(如果没有继承它需要在自己的 Realm 中自己实现验证);
2、提供给 SecurityManager 来创建 Subject(提供身份信息);
MergableAuthenticationInfo 用于提供在多 Realm 时合并 AuthenticationInfo 的功能,主要合
并 Principal、如果是其他的如 credentialsSalt,会用后边的信息覆盖前边的。
比 如 HashedCredentialsMatcher , 在 验 证 时 会 判 断 AuthenticationInfo 是 否 是
SaltedAuthenticationInfo 子类,来获取盐信息。
Account 相当于我们之前的 User,SimpleAccount 是其一个实现;在 IniRealm、PropertiesRealm
这种静态创建帐号信息的场景中使用,这些 Realm 直接继承了 SimpleAccountRealm,而
SimpleAccountRealm 提供了相关的 API 来动态维护 SimpleAccount;即可以通过这些 API
来动态增删改查 SimpleAccount;动态增删改查角色/权限信息。及如果您的帐号不是特别
多,可以使用这种方式,具体请参考 SimpleAccountRealm Javadoc。
其他情况一般返回 SimpleAuthenticationInfo 即可。
PrincipalCollection
截止到目前为止,只定义了一个Realm,String userName =(String) principalCollection.getPrimaryPrincipal();自然取到了我们想要的身份信息
但是我们可以在 Shiro 中同时配置多个 Realm,所以呢身份信息可能就有多个;因此其提供了 PrincipalCollection 用于聚合这些身份信息:
public interface PrincipalCollection extends Iterable, Serializable {
Object getPrimaryPrincipal(); //得到主要的身份
<T> T oneByType(Class<T> type); //根据身份类型获取第一个
<T> Collection<T> byType(Class<T> type); //根据身份类型获取一组
List asList(); //转换为 List
Set asSet(); //转换为 Set
Collection fromRealm(String realmName); //根据 Realm 名字获取
Set<String> getRealmNames(); //获取所有身份验证通过的 Realm 名字
boolean isEmpty(); //判断是否为空
}
因为 PrincipalCollection 聚合了多个,此处最需要注意的是 getPrimaryPrincipal,如果只有一
个 Principal 那么直接返回即可,如果有多个 Principal,则返回第一个(因为内部使用 Map
存储,所以可以认为是返回任意一个);
oneByType / byType 根据凭据的类型返回相应的Principal;
fromRealm 根据 Realm 名字(每个 Principal 都与一个 Realm 关联)获取相应的Principal。
多Realm使用场景
比如不同角色,使用不同的登录策略
AuthorizationInfo 授权对象
AuthorizationInfo 用于聚合授权信息的:
public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //获取角色字符串信息
Collection<String> getStringPermissions(); //获取权限字符串信息
Collection<Permission> getObjectPermissions(); //获取 Permission 对象信息
}
当 我 们 使 用 AuthorizingRealm 时 , 如 果 身 份 验 证 成 功 , 在 进 行 授 权 时 就 通 过
doGetAuthorizationInfo 方法获取角色/权限信息用于授权验证。
Shiro 提供了一个实现 SimpleAuthorizationInfo,大多数时候使用这个即可。
Subject
Subject 是 Shiro 的核心对象,基本所有身份验证、授权都是通过 Subject 完成
对于 Subject 的构建一般没必要我们去创建;一般通过 SecurityUtils.getSubject()获取
源码:即首先查看当前线程是否绑定了 Subject,如果没有通过 Subject.Builder 构建一个然后绑定到现场返回
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
用法:
1、身份信息获取
Object getPrincipal(); //Primary Principal
PrincipalCollection getPrincipals(); // PrincipalCollection
2、身份验证
void login(AuthenticationToken token) throws AuthenticationException;
boolean isAuthenticated();
boolean isRemembered();
注明:通过 login 登录,如果登录失败将抛出相应的 AuthenticationException,如果登录成功调用
isAuthenticated 就会返回 true,即已经通过身份验证;如果 isRemembered 返回 true,表示是
通过记住我功能登录的而不是调用 login 方法登录的。isAuthenticated/isRemembered 是互斥
的,即如果其中一个返回 true,另一个返回 false。
3.角色授权验证
boolean hasRole(String roleIdentifier);
boolean[] hasRoles(List<String> roleIdentifiers);
boolean hasAllRoles(Collection<String> roleIdentifiers);
void checkRole(String roleIdentifier) throws AuthorizationException;
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(String... roleIdentifiers) throws AuthorizationException;
hasRole*进行角色验证,验证后返回 true/false;
而 checkRole*验证失败时抛出AuthorizationException 异常 ,别瞎用,会造成无法登陆
4、权限授权验证
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
isPermitted*进行权限验证,验证后返回 true/false;而 checkPermission*验证失败时抛出
AuthorizationException。和角色授权认证类似.
5、会话
Session getSession(); //相当于 getSession(true)
Session getSession(boolean create);
登录成功就创建了一个会话,Session session = subject.getSession()获取
如果 create=true 如果没有会话将返回 null,而 create=true 如果没有会话会强制创建一个。
6.退出
void logout();
7、RunAs
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException;
boolean isRunAs();
PrincipalCollection getPreviousPrincipals();
PrincipalCollection releaseRunAs();
RunAs 即实现“允许 A 假设为 B 身份进行访问”;通过调用 subject.runAs(b)进行访问;接
着调用 subject.getPrincipals 将获取到 B 的身份;此时调用 isRunAs 将返回 true;而 a 的身
份需要通过 subject. getPreviousPrincipals 获取;如果不需要 RunAs 了调用 subject.
releaseRunAs 即可
8、多线程
<V> V execute(Callable<V> callable) throws ExecutionException;
void execute(Runnable runnable);
<V> Callable<V> associateWith(Callable<V> callable);
Runnable associateWith(Runnable runnable);
实现线程之间的 Subject 传播,因为 Subject 是线程绑定的;因此在多线程执行中需要传播
到相应的线程才能获取到相应的 Subject。最简单的办法就是通过 execute(runnable/callable
实例)直接调用;或者通过 associateWith(runnable/callable 实例)得到一个包装后的实例;它
们都是通过:1、把当前线程的 Subject 绑定过去;2、在线程执行结束后自动释放。
Subject 自己不会实现相应的身份验证/授权逻辑,而是通过 DelegatingSubject 委托给
SecurityManager 实现;及可以理解为 Subject 是一个面门
代码已上传github :https://github.com/currynice/shiro-ssm