学习视频:尚硅谷Shiro安全权限框架实战教程全套完整版(从入门到精通)
真正的大师永远保持着一颗学徒的心
目录
文章目录
快速开始
Shiro集成Spring
这个视频是使用SpringMVC,这里我使用Springboot进行集成,本质差不多
学习资料下载:https://downloads.apache.org/shiro/1.5.3/shiro-root-1.5.3-source-release.zip
具体细节在上篇学习笔记
- 配置DefaultWebSecurityManager
- 配置CacheManage(不重要)
- 需要加入ehcache的jar包和配置文件
- 配置Realm
- 直接实现Realm接口
- 配置LifecycleBeanPostProcessor,可以自动的调用配置在Spring IOC 容器中的 Shiro bean 的声明周期方法
- 启用IOC容器中使用Shiro注解,但必须配配置LifecycleBeanPostProcessor之后才可以使用
- 配置ShiroFilter
理论上的配置
@Configuration
public class ShiroConfig {
public RolesRealm realm() {
RolesRealm realm = new RolesRealm();
return realm;
}
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
return shiroFilterFactoryBean;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@DependsOn(value = "lifecycleBeanPostProcessor")
@Bean
public DefaultAdvisorAutoProxyCreator DefaultAdvisorAutoProxyCreator() {
return new DefaultAdvisorAutoProxyCreator();
}
}
注意:如果集成Springboot,本来需要在
web.xml
中配置的拦截器不需要配置
URL匹配模式
Url规则使用Ant模式
Ant路径通配符支持?、*、**
,注意通配符匹配不包括目录分隔符/:
- ?:匹配一个字符,如
/admin?
匹配/admin1
,但不匹配/admin
,/admin/
- *:匹配零个或多个字符串,如
/admin
将匹配/admin
和/admin123
,但不匹配/admin/a
- **:匹配路径中的零个或多个路径,如
/admin/**
将匹配/admin/a
或/admin/b
URL匹配顺序
- URL权限采取第一次匹配优先的方式,即从头开始使用第一个匹配的URL模式对应的拦截器链
- 如
- /bb/** = filter1
- /bb/aa = filter2
- /** = filter3
- 如果请求的URL是
/bb/aa
,因为按照声明顺序进行匹配,那么将使用filter1进行拦截
认证
- 获取当前Subject,调用
SecurityUtls.getSubject()
方法 - 测试当前用户是否已被认证,调用
subject.isAuthenticated()
方法 - 若没有被认证,则将用户名密码封装为一个
UsernamePasswordToken
对象 - 执行登录,调用
subject.login(Authenticateion)
方法 - 自定义Realm方法,从数据库中获取对应的记录返回给Shiro
- 继承
org.apache.shiro.realm.AuthorizingRealm
类 - 实现
doGetAuthorizationInfo
和doGetAuthenticationInfo
方法
- 继承
- 由Shiro完成密码的验证
具体实现参考:Shiro入门.md
public class RolesRealm extends AuthorizingRealm {
@Autowire
private UserDao userDao;
@Override
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
//认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1. 将AuthenticationToken对象强制转换成UsernamePasswordToken
UsernamePasswordToken selfToken = (UsernamePasswordToken)token;
//2. 从UsernamePasswordToken中获取用户名
String username = selfToken.getUsername();
//3. 从数据库中查询用户信息
User user = userDao.queryByUsername(username);
//4. 若用户不存在,则抛出UnknownAccoutException异常,返回null就会自动抛出
if (user == null) {
throw new UnknownAccountException();
}
//5. 根据用户情况决定是否抛出AuthenticationException异常
//6. 根据用户情况,构建AuthenticationInfo对象并返回
return new SimpleAuthenticationInfo(user, user.getPassword(), "");
}
}
SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
参数:
- Object principal:用户实体信息
- Object credentials:用户密码信息
- String realmName认证的realm名字,多realm认证时使用
密码的比对
通过AuthenticatingRealm
类的CredentialsMatcher
属性进行比对的
使用密码加密
使用MD5加密
替换当前Realm的CredentialsMatcher
属性,直接使用HashedCredentialsMatcher
对象,并调用setHashAlgorithmName
方法设置加密算法即可
@Configuration
public class ShiroConfig {
@Bean
public RolesRealm realm() {
RolesRealm realm = new RolesRealm();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("MD5");
//加密的名称
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密的次数
hashedCredentialsMatcher.setHashIterations(1);
//是否使用盐值加密
//这个不需要手动设置,他会根据Realm的返回值自行判断
hashedCredentialsMatcher.setHashSalted(false);
//加密的结果是32位小写
realm.setCredentialsMatcher(hashedCredentialsMatcher);
return realm;
}
}
使用MD5盐值加密
@Bean
public RolesRealm realm() {
RolesRealm realm = new RolesRealm();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("MD5");
//加密的名称
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密的次数
hashedCredentialsMatcher.setHashIterations(1);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
return realm;
}
@Override
//认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = "admin";
String password = "f6fdffe48c908deb0f4c3bd36c032e72";
UsernamePasswordToken selfToken = (UsernamePasswordToken)token;
if (!username.equals(selfToken.getUsername())) {
throw new UnknownAccountException("用户不存在");
}
//构建盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
//返回可以使用盐值加密的AuthenticationInfo的实现类
return new SimpleAuthenticationInfo(username, password, credentialsSalt, "");
}
//获取加密后的值
public static void main(String[] args) {
System.err.println(new SimpleHash("MD5", "admin", "admin", 1));
}
为什么使用MD5盐值加密
我们希望即使两个人的密码一样,但是加密后的值也不一样
怎么做到
- 在自定义Realm的
doGetAuthenticationInfo
方法的返回值返回SimpleAuthenticationInfo
实现类,并使用public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName)
构造器 - 使用
ByteSource.Util.bytes()
方法来计算盐值 - 盐值需要唯一,一般的使用随机字符串(比如UUID)或user_id
- 使用
SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)
方法可以获取盐值加密后的字符串
多Realm验证和认证策略
多Realm验证
- 新增一个Realm
- 修改配置文件
@Configuration
public class ShiroConfig {
@Bean
public SaltRealm saltRealm() {
SaltRealm realm = new SaltRealm();
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher("MD5");
//加密的名称
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密的次数
hashedCredentialsMatcher.setHashIterations(1);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
return realm;
}
@Bean
public Sha1Realm sha1Realm() {
Sha1Realm sha1Realm = new Sha1Realm();
sha1Realm.setCredentialsMatcher(new HashedCredentialsMatcher("SHA1"));
return sha1Realm;
}
//多Realm认证器
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(
SaltRealm saltRealm,
Sha1Realm sha1Realm) {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
Collection<Realm> realms = new ArrayList<>();
realms.add(saltRealm);
realms.add(sha1Realm);
authenticator.setRealms(realms);
return authenticator;
}
@Bean
public DefaultWebSecurityManager securityManager(ModularRealmAuthenticator modularRealmAuthenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setAuthenticator(modularRealmAuthenticator);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//省略。。。
return shiroFilterFactoryBean;
}
}
在每个Realm的认证方法内加入System.err.println("进入MD5盐值验证");
启动项目
进行认证,就可以在控制台看到
进入MD5盐值验证
进入SHA1验证
认证策略
AuthenticationStrategy
接口的默认实现:
FirstSuccessfulStrategy
:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略AtLeastOneSuccessfulStrategy
:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息AllSuccessfulStrategy
:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败了就全部失败了
ModularRealmAuthenticator默认使用AtLeastOneSuccessfulStrategy策略
@Configuration
public class ShiroConfig {
//多Realm认证器
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(
SaltRealm saltRealm,
Sha1Realm sha1Realm) {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
Collection<Realm> realms = new ArrayList<>();
realms.add(saltRealm);
realms.add(sha1Realm);
authenticator.setRealms(realms);
//设置认证策略
authenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
return authenticator;
}
}
设置多Realm的第二种方法
不去实现ModularRealmAuthenticator
类,直接将两个Realm配置在DefaultWebSecurityManager
中
@Bean
public DefaultWebSecurityManager securityManager(Sha1Realm sha1Realm, SaltRealm saltRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
Collection<Realm> realms = new ArrayList<>();
realms.add(saltRealm);
realms.add(sha1Realm);
securityManager.setRealms(realms);
return securityManager;
}
授权
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UD8xYPsf-1594904737897)(2548B239B06C4BED96222658A0216006)]
授权方式
Shiro支持三种授权方式
- 编程式:通过写if/else授权代码完成
- 注解式:通过在执行的java方法上放置相应的注解完成,没有权限则会抛出相应的异常
- JSP/GSP标签:在JSP/GSP页面通过相应的标签完成
授权书写规则
标识符:操作:对象实例ID
,即对哪个资源的哪个实例可以进行什么操作,其默认支持通配符权限字符串,:
:表示资源/操作/实例的分割,
:表示操作的分割*
:表示任意资源/操作/实例
- 多层次管理
- 例如:user:add、user:delete
- 冒号是一种特殊字符,它用来分割权限字符串的下一部件:第一部分是权限被操作的领域,第二部分是被执行的操作
- 多个值:每个部件能够保护多个值,因此,除了授予用户user:add和user:delete权限外,也可以简单的授予他们一个user:add,delete
- 还可以用*代表所有值,如user: *,也可以写成: *:delete,表示某个用户在所有领域都拥有delete权限
- 部分省略通配符:缺少的部件意味着用户可以访问所有与之匹配的值,比如:user:add等价于user:add:*,user等价于user: *: *
- 注意:通配符只能从字符串的结尾处省略部件,也就是说user:add并不等价于user:*:add
简单权限授权
授权需要继承org.apache.shiro.realm.AuthorizingRealm
,并实现抽象方法doGetAuthorizationInfo
//直截取授权
public class SaltRealm extends AuthorizingRealm {
@Override
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
System.err.println("进行认证");
Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
//通过username查询获取对应的权限,再将权限加入
info.addStringPermission("user:delete");
//返回认证信息
return info;
}
}
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new LinkedHashMap<>();
map.put("/", "anon");
map.put("/user/delete", "perms[user:delete]");
map.put("/user/**", "authc");
return shiroFilterFactoryBean;
}
}
写的确实有点乱,有问题可以评论
简单角色验证
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//map
Map<String, String> map = new LinkedHashMap<>();
map.put("/", "anon");
map.put("/user/delete", "perms[user:delete]");
map.put("/user/**", "authc");
map.put("/admin/**", "roles[admin]");
//添加shiro内置的过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//用户未登录不进行跳转,而是自定义返回json数据
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters
filters.put("authc", new ShiroLoginFilter());//将自定义 的FormAuthenticationFilter注入shiroFilter中
filters.put("perms", new ShiroPermsFilter());
filters.put("roles", new ShiroRolesFilter());
return shiroFilterFactoryBean;
}
}
public class SaltRealm extends AuthorizingRealm {
@Override
//授权
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//这里注意,如果你是多Realm认证,而且认证策略是AtLeastOneSuccessfulStrategy
//这里获取的就是第一个Principal,或者说是随机的
String username = (String) principals.getPrimaryPrincipal();
Collection<String> roles = new ArrayList<String>();
if (username.equals("user")) {
info.addStringPermission("user:delete");
roles.add("user");
}else if(username.equals("admin")) {
info.addStringPermission("admin:*");
roles.add("admin");
}else {
return null;
}
info.addRoles(roles);
return info;//info;
}
}
Shiro标签
这里视频中主要讲解JSP标签,并没有讲解Thymeleaf标签,而且我是做前后端分离的,所以跳过,有兴趣的可以百度:Shiro与Thymeleaf整合,这里不做过多介绍
权限注解
@RequiresAuthentication
:表示当前Subject已经通过login进行了身份验证,即subject.isAuthenticated()
返回true
@RequiresUser
:表示当前Subject已经身份验证或通过记住我登录的
@RequiresGuest
:表示当前Subject没有身份验证或通过记住我登录过,即游客身份
@RequiresRoles(value = {"user", "admin"}, logical = Logical.AND)
:表示当前Subject需要角色user和admin
@RequiresPermissions(value = {"user:add", "user:delete"}, logical = Logical.OR)
:表示当前Subject需要权限user:add和user:delete
新增配置,开启注解
@Configuration
public class ShiroConfig {
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
使用注解
@RestController
@RequestMapping("admin")
public class AdminController {
@RequiresPermissions(value = "admin:delete")
@GetMapping("delete")
public String delete() {
return "admin:delete";
}
}
当访问/admin/delete的时候,会抛出UnauthorizedException异常,这时候我们可以使用Spring的声明式异常进行捕获,做出相应的响应
@RestControllerAdvice
public class ShiroException {
//授权错误
@ExceptionHandler(AuthorizationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public String handleException(AuthorizationException e) {
return "授权错误";
}
}
特别注意
这个注解可以加在Service层,但在实际使用的过程中,我们常常会在Service层增加事务的注解,这时候这个方法会被代理,我们就不能将这个注解加载Service层,而应加在Controller层,我们不能让一个方法成为代理的代理,会抛出类型转换异常,大家在开发的时候需要注意一下
从数据表中初始化资源和权限
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new LinkedHashMap<>();
//在这里读取数据库之后加入就好了
return shiroFilterFactoryBean;
}
}
会话管理
在 Shio 中,Session 是一个特定的实例,它不仅仅代表普通的 HTTPSession,而且它与 HTTPSession 最大的不同就是,Shiro Session 不依赖 HTTP 环境。
在 Web 环境中,默认情况下,Shiro Session 就是 HTTPSession,但在非 Web 环境下,Shiro 会自动使用企业级会话管理。也就是说,在 Web 应用中,你可以使用同一套 Session API,而不用担心应用的发布环境。
会话相关的API
subject.getSession()
:即可获取会话;其等价于subject.getSession(true)
,即如果当前没有创建Session对象会创建一个;subject.getSession(false)
,如果当前没有创建Session对象则返回null
session.getId()
:获取当前会话的唯一表示
session.getHost()
:获取当前Subject的主机地址
session.getTimeout() & session.getTimeout(毫秒数)
:获取/设置当前Session的过期时间
session.getStartTimestamp() & session.getLastAccessTime()
:获取会话的启动时间及最后访问时间。如果JavaSE应用需要自己定期调用session.touch()
去更新最后访问时间。如果是Web应用,每次进入ShiroFilter
都会自动调用session.touch()
来更新最后访问时间
session.touch() & session.stop()
:更新会话最后访问时间及销毁会话,当Subject.logout()
时会自动调用stop方法来销毁会话。如果在Web中,调用HttpSession.invalidate()
也会自动调用Shiro Session.stop()
方法进行销毁Shiro的会话
session.setAttribute(key, value) & session.getAttribute(key) & session.removeAttribute(key)
:设置/获取/删除会话属性,在整个会话范围内都可以对这些属性进行操作
会话监听器
会话监听器用于监听会话创建、过期及停止操作
|-SessionListener
|----onStart(Session):void
|----onStop(Session):void
|----onExpiration(Session):void
建议在Web应用使用HttpSessionListener
,SessionListener
存在的意义是在JavaSE环境下使用
最后
Shiro提供的Session还有个作用,就是在Service层,我们不需要通过传参也可以获取HttpSession,Shiro的Session和HttpSession是互通的
SessionDao
跳过
缓存
跳过
认证和记住我的区别
Shiro提供了记住我功能,当我们登录时选择记住我,重新打开浏览器再次访问页面时,仍然显示登录状态
区别
subject.isAuthenticated()
:表示用户进行了身份验证登录的,即使用Subject.login()
进行了登录
subject.isRemembered()
:表示用户是通过记住我登录,此时可能不是真正的你(可能是朋友在用你的电脑,也可能何可获取了你的Session)在访问
两者二选一,即subject.isAuthenticated() == true
,则subject.isRemembered() == false
;反之亦然
建议
在访问一般网页时,认证和记住我验证一个就可以,但是在访问特殊网页的使用,只能认证成功才能访问