Shiro TabTan
本篇笔记记录于2021年3月,迁移至此,分享给需要它的人。
Shiro的核心功能
Authentication、Authorization、Session Management、Cryptography
Authentication 认证
Authorization 授权
Session Management 会话管理
Cryptography 加密
支持的特性:
- Web Support -Shiro提供过滤器
- Caching 缓存支持
- Concurrency 支持多线程应用
- Testing 提供测试支持
- Run As允许一个用户以另一种身份去访问
- Remember Me
说明:Shiro是一个安全框架,不提供用户及权限的维护
Shiro的核心组件
Subject、Security Manager、Realms
- Subject 表示待认证和授权的用户
- Security Manager 安全管理器
- Authenticator 认证器
- Authorizer 授权器
- SessionManager 会话管理器
- CacheManager 缓存管理器
- Realm 相当于Shiro进行认证和授权的数据源,充当了Shiro与安全数据之间的桥梁或者连接器。
Hello World程序
实例一:JavaSE使用shiro(数据从文件.ini导入)
导入依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.6.0</version>
</dependency>
ini:
[users]
zhangsan=123456,seller
admin=123456,ckmgr
admin=123456,admin
[roles]
admin=*
seller=order-add,order-del,order-list
ckmgr=ck-add,ck-del,ck-list
主程序:
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入账号:");
String username = scanner.nextLine();
System.out.println("请输入密码:");
String password = scanner.nextLine();
DefaultSecurityManager manager = new DefaultSecurityManager();
IniRealm realm = new IniRealm("classpath:test.ini");
manager.setRealm(realm);
SecurityUtils.setSecurityManager(manager);
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = new UsernamePasswordToken(username,password);
System.out.println(subject.isAuthenticated());
subject.login(token);
System.out.println(subject.isAuthenticated());
System.out.println(subject.hasRole("admin"));
}
结果
请输入账号:
admin
请输入密码:
123456
false
true
true
步骤:
- 创建安全管理器SecurityManager
- 创建realm
- 将realm设置到安全管理器
- 将安全管理器设置到工具类
- 从工具类获取subject
- 获取token,登录
流程:
① 通过subject.login(token)进行登录,就会将token包含的用户信息(账号密码)传递给SecurityManager
② SecurityManager调用Anthenticator进行身份认证
③ Anthenticator把token传递给Realm
④ Realm根据得到的token,调用dogetAuthenticationinfo方法进行认证(如果认证失败则抛出异常)
Shiro实例
pom.xml 依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
<scope>compile</scope>
</dependency>
application.yml
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
initial-size: 1
min-idle: 1
max-active: 20
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.tan.pojo
ShiroConfig.java
@Configuration
public class ShiroConfig {
//方言
@Bean
ShiroDialect dialect(){
return new ShiroDialect();
}
//自定义Realm
@Bean
MyUserRealm userRealm(HashedCredentialsMatcher hashedCredentialsMatcher){
MyUserRealm userRealm = new MyUserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return userRealm;
}
//自定义Realm
@Bean
MyAdminRealm adminRealm(HashedCredentialsMatcher credentialsMatcher){
MyAdminRealm adminRealm = new MyAdminRealm();
adminRealm.setCredentialsMatcher(credentialsMatcher);
return adminRealm;
}
//加盐
@Bean
HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("md5");
matcher.setHashIterations(3);
return matcher;
}
@Bean
MyModularRealmAuthenticator modularRealmAuthenticator(){
return new MyModularRealmAuthenticator();
}
//安全管理器
@Bean
DefaultWebSecurityManager securityManager(HashedCredentialsMatcher credentials){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
Collection<Realm> realms = new ArrayList<>();
realms.add(userRealm(credentials));
realms.add(adminRealm(credentials));
manager.setAuthenticator(modularRealmAuthenticator());
manager.setRealms(realms);
return manager;
}
//过滤器工厂Bean
@Bean
ShiroFilterFactoryBean filter(DefaultWebSecurityManager manager){
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(manager);
filter.setLoginUrl("/login");
filter.setUnauthorizedUrl("/lessPermission");
Map<String,String> map = new HashMap<>();
map.put("/", "anon");
map.put("/toLogin","anon");
map.put("/login", "anon");
map.put("/regist", "anon");
map.put("/tan","anon");
map.put("/layui/**","anon");
map.put("/layui/layui.js","anon");
map.put("/index", "user");
map.put("/index1", "user");
map.put("/index2", "user");
map.put("/logout","logout");
map.put("/cAdd","perms[sys:c:save]");
map.put("/**","authc");
filter.setFilterChainDefinitionMap(map);
return filter;
}
}
自定义Token
MyToken.java
public class MyToken extends UsernamePasswordToken {
private String loginType;
public MyToken(String username,String password,String loginType){
super(username,password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
自定义Realm
MyAdminRealm.java
public class MyAdminRealm extends AuthorizingRealm {
private Logger log = LoggerFactory.getLogger(MyAdminRealm.class);
@Resource
UserDAO userDAO;
@Resource
PrimissionDAO primissionDAO;
@Resource
RoleDAO roleDAO;
@Override
public String getName() {
return "admin";
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.iterator().next();
Set<String> ps = primissionDAO.getPrimissionsByUsername(username);
Set<String> rs = roleDAO.getRoleNamesByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rs);
info.setStringPermissions(ps);
return info;
}
@SneakyThrows
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("--------------adminRealm--------------");
UsernamePasswordToken up = (UsernamePasswordToken) token;
User user = userDAO.queryUserByUsername(((UsernamePasswordToken) token).getUsername());
ByteSource salt = ByteSource.Util.bytes(user.getPasswordSalt());
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),salt,getName());
}
}
MyUserRealm.java
public class MyUserRealm extends AuthorizingRealm {
private Logger log = LoggerFactory.getLogger(MyUserRealm.class);
@Resource
UserDAO userDAO;
@Resource
PrimissionDAO primissionDAO;
@Resource
RoleDAO roleDAO;
@Override
public String getName() {
return "user";
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.iterator().next();
Set<String> rs = roleDAO.getRoleNamesByUsername(username);
Set<String> ps = primissionDAO.getPrimissionsByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(rs);
info.setStringPermissions(ps);
return info;
}
@SneakyThrows
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("--------------userRealm--------------");
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
User user = userDAO.queryUserByUsername(upToken.getUsername());
ByteSource bytes = ByteSource.Util.bytes(user.getPasswordSalt());
return new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),bytes,getName());
}
}
自定义认证器MyModularRealmAuthenticator.java
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator {
private Logger log = LoggerFactory.getLogger(MyModularRealmAuthenticator.class);
//自定义doAuthenticate方法
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("------自定义Authenticator------");
assertRealmsConfigured();
//将token强转成自定义的带有loginType的MyToken
MyToken token = (MyToken) authenticationToken;
Collection<Realm> realms = getRealms();
Collection<Realm> typeRealms = new ArrayList<>();
for (Realm realm:realms) {
String name = realm.getName();
if (realm.getName().startsWith(token.getLoginType())){
typeRealms.add(realm);
}
}
if (typeRealms.size() == 1) {
return doSingleRealmAuthentication((Realm) typeRealms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(typeRealms, authenticationToken);
}
}
}
入口:
public void checkLogin(String username,String password,boolean rememberMe,String loginType) throws Exception {
Subject subject = SecurityUtils.getSubject();
//使用自定义token
UsernamePasswordToken token = new
MyToken(username,password,"admin".equals(loginType)?"admin":"user");
subject.login(token);
}
登录表单:
<form action="/toLogin" method="post">
<span>账号:</span><input name="username"/>
<span>密码:</span><input type="password" name="password"/>
<br>
<input name="loginType" type="radio" checked="checked" value="admin">管理员
<input name="loginType" type="radio" value="user">普通用户
<span>记住我:</span><input type="checkbox" name="rememberMe" />
<button type="submit">登录</button>
</form>
详解
认证
授权
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
三种方式
-
if方式
-
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }
-
-
注解方式
-
@RequiresRoles("admin") public void hello() { //有权限 }
-
-
命名空间的标签方式
-
<shiro:hasRole name="admin"> <!— 有权限 —> </shiro:hasRole>
-
粗粒度
Shiro 提供了 hasRole/hasRoles 用于判断用户是否拥有某个角色/某些权限;
Shiro 提供的 checkRole/checkRoles 和 hasRole/hasAllRoles 不同的地方是它在判断为假的情况下会抛出 UnauthorizedException 异常。
细粒度
Shiro 提供了 isPermitted 和 isPermittedAll 用于判断用户是否拥有某个权限或所有权限,也没有提供如 isPermittedAny 用于判断拥有某一个权限的接口。但是失败的情况下会抛出 UnauthorizedException 异常。
字符串通配符权限
规则:“资源标识符:操作:对象实例 ID” 即对哪个资源的哪个实例可以进行什么操作。其默认支持通配符权限字符串,“:”表示资源/操作/实例的分割;“,”表示操作的分割;“*”表示任意资源/操作/实例。
用户拥有所有资源的“view”所有权限。假设判断的权限是“"system:user:view”,那么需要“role5=::view”这样写才行
授权流程
- 首先调用 Subject.isPermitted*/hasRole*接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
- Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;
- 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
- Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。
ModularRealmAuthorizer 进行多 Realm 匹配流程:
- 首先检查相应的 Realm 是否实现了实现了 Authorizer;
- 如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
- 如果有一个 Realm 匹配那么将返回 true,否则返回 false。
如果 Realm 进行授权的话,应该继承 AuthorizingRealm,其流程是:
- 如果调用 hasRole*,则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可;首先如果调用如 isPermitted(“user:view”),首先通过 PermissionResolver 将权限字符串转换成相应的 Permission 实例,默认使用 WildcardPermissionResolver,即转换为通配符的 WildcardPermission;
- 通过 AuthorizationInfo.getObjectPermissions() 得到 Permission 实例集合;通过 AuthorizationInfo.getStringPermissions() 得到字符串集合并通过 PermissionResolver 解析为 Permission 实例;然后获取用户的角色,并通过 RolePermissionResolver 解析角色对应的权限集合(默认没有实现,可以自己提供);
- 接着调用 Permission.implies(Permission p) 逐个与传入的权限比较,如果有匹配的则返回 true,否则 false。
Authorizer、PermissionResolver及RolePermissionResolver
Authorizer 的职责是进行授权(访问控制),PermissionResolver 用于解析权限字符串到 Permission 实例,而 RolePermissionResolver 用于根据角色解析相应的权限集合。
会话
基本操作
登录成功后使用 Subject.getSession() 即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。
session.getId();
获取当前会话的唯一标识。
session.getHost();
获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost() 提供的。
session.getTimeout();
session.setTimeout(毫秒);
获取 / 设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。
session.getStartTimestamp();
session.getLastAccessTime();
获取会话的启动时间及最后访问时间;如果是 JavaSE 应用需要自己定期调用 session.touch() 去更新最后访问时间;如果是 Web 应用,每次进入 ShiroFilter 都会自动调用 session.touch() 来更新最后访问时间。
session.touch();
session.stop();
更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。如果在 web 中,调用 javax.servlet.http.HttpSession. invalidate() 也会自动调用 Shiro Session.stop 方法进行销毁 Shiro 的会话。
session.setAttribute("key", "123");
Assert.assertEquals("123", session.getAttribute("key"));
session.removeAttribute("key");
设置 / 获取 / 删除会话属性;在整个会话范围内都可以对这些属性进行操作。
会话管理器
会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了 SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。
SecurityManager 提供了如下接口:
Session start(SessionContext context); //启动会话
Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话
另外用于 Web 环境的 WebSessionManager 又提供了如下接口:
boolean isServletContainerSessions();// 是否使用 Servlet 容器的会话
Shiro 还提供了 ValidatingSessionManager 用于验资并过期会话:
void validateSessions();// 验证所有会话是否过期
三个默认实现
DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;
DefaultWebSessionManager:用于 Web 环境的实现,可以替代 ServletContainerSessionManager,自己维护着会话,直接废弃了 Servlet 容器的会话管理。
SimpleCookie
- sessionIdCookie 是 sessionManager 创建会话 Cookie 的模板:
- sessionIdCookie.name:设置 Cookie 名字,默认为 JSESSIONID;
- sessionIdCookie.domain:设置 Cookie 的域名,默认空,即当前访问的域名;
- sessionIdCookie.path:设置 Cookie 的路径,默认空,即存储在域名根下;
- sessionIdCookie.maxAge:设置 Cookie 的过期时间,秒为单位,默认 - 1 表示关闭浏览器时过期 Cookie;
- sessionIdCookie.httpOnly:如果设置为 true,则客户端不会暴露给客户端脚本代码,使用 HttpOnly cookie 有助于减少某些类型的跨站点脚本攻击;此特性需要实现了 Servlet 2.5 MR6 及以上版本的规范的 Servlet 容器支持;
- sessionManager.sessionIdCookieEnabled:是否启用 / 禁用 Session Id Cookie,默认是启用的;如果禁用后将不会设置 Session Id Cookie,即默认使用了 Servlet 容器的 JSESSIONID,且通过 URL 重写(URL 中的 “;JSESSIONID=id” 部分)保存 Session Id。
另外我们可以如 “sessionManager. sessionIdCookie.name=sid” 这种方式操作 Cookie 模板。
会话监听器
会话监听器用于监听会话创建、过期及停止事件:
public class MySessionListener1 implements SessionListener {
@Override
public void onStart(Session session) {//会话创建时触发
System.out.println("会话创建:" + session.getId());
}
@Override
public void onExpiration(Session session) {//会话过期时触发
System.out.println("会话过期:" + session.getId());
}
@Override
public void onStop(Session session) {//退出/会话过期时触发
System.out.println("会话停止:" + session.getId());
}
}
如果只想监听某一个事件,可以继承 SessionListenerAdapter 实现:
public class MySessionListener2 extends SessionListenerAdapter {
@Override
public void onStart(Session session) {
System.out.println("会话创建:" + session.getId());
}
}
在 shiro-web.ini 配置文件中可以进行如下配置设置会话监听器:
sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
sessionManager.sessionListeners=$sessionListener1,$sessionListener2
会话存储 / 持久化
Shiro 提供 SessionDAO 用于会话的 CRUD,即 DAO(Data Access Object)模式实现
会话验证
Shiro 提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在 web 环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro 提供了会话验证调度器 SessionValidationScheduler 来做这件事情。
Shiro 缓存机制
Shiro 提供了类似于 Spring 的 Cache 抽象,即 Shiro 本身不实现 Cache,但是对 Cache 进行了又抽象,方便更换不同的底层 Cache 实现。
public interface Cache<K, V> {
//根据Key获取缓存中的值
public V get(K key) throws CacheException;
//往缓存中放入key-value,返回缓存中之前的值
public V put(K key, V value) throws CacheException;
//移除缓存中key对应的值,返回该值
public V remove(K key) throws CacheException;
//清空整个缓存
public void clear() throws CacheException;
//返回缓存大小
public int size();
//获取缓存中所有的key
public Set<K> keys();
//获取缓存中所有的value
public Collection<V> values();
}
public interface CacheManager {
//根据缓存名字获取一个Cache
public <K, V> Cache<K, V> getCache(String name) throws CacheException;
}
public interface CacheManagerAware {
//注入CacheManager
void setCacheManager(CacheManager cacheManager);
}