Shiro正式学习

Shiro入门

学习视频:尚硅谷Shiro安全权限框架实战教程全套完整版(从入门到精通)
真正的大师永远保持着一颗学徒的心

目录

快速开始

Shiro集成Spring

这个视频是使用SpringMVC,这里我使用Springboot进行集成,本质差不多
学习资料下载:https://downloads.apache.org/shiro/1.5.3/shiro-root-1.5.3-source-release.zip

具体细节在上篇学习笔记

  1. 配置DefaultWebSecurityManager
  2. 配置CacheManage(不重要)
    1. 需要加入ehcache的jar包和配置文件
  3. 配置Realm
    1. 直接实现Realm接口
  4. 配置LifecycleBeanPostProcessor,可以自动的调用配置在Spring IOC 容器中的 Shiro bean 的声明周期方法
  5. 启用IOC容器中使用Shiro注解,但必须配配置LifecycleBeanPostProcessor之后才可以使用
  6. 配置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进行拦截

认证

  1. 获取当前Subject,调用SecurityUtls.getSubject()方法
  2. 测试当前用户是否已被认证,调用subject.isAuthenticated()方法
  3. 若没有被认证,则将用户名密码封装为一个UsernamePasswordToken对象
  4. 执行登录,调用subject.login(Authenticateion)方法
  5. 自定义Realm方法,从数据库中获取对应的记录返回给Shiro
    1. 继承org.apache.shiro.realm.AuthorizingRealm
    2. 实现doGetAuthorizationInfodoGetAuthenticationInfo方法
  6. 由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盐值加密

我们希望即使两个人的密码一样,但是加密后的值也不一样

怎么做到

  1. 在自定义Realm的doGetAuthenticationInfo方法的返回值返回SimpleAuthenticationInfo实现类,并使用public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName)构造器
  2. 使用ByteSource.Util.bytes()方法来计算盐值
  3. 盐值需要唯一,一般的使用随机字符串(比如UUID)或user_id
  4. 使用SimpleHash(String algorithmName, Object source, Object salt, int hashIterations)方法可以获取盐值加密后的字符串

多Realm验证和认证策略

多Realm验证

  1. 新增一个Realm
  2. 修改配置文件
@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应用使用HttpSessionListenerSessionListener存在的意义是在JavaSE环境下使用

最后

Shiro提供的Session还有个作用,就是在Service层,我们不需要通过传参也可以获取HttpSession,Shiro的Session和HttpSession是互通的

SessionDao

跳过

缓存

跳过

认证和记住我的区别

Shiro提供了记住我功能,当我们登录时选择记住我,重新打开浏览器再次访问页面时,仍然显示登录状态

区别

subject.isAuthenticated():表示用户进行了身份验证登录的,即使用Subject.login()进行了登录
subject.isRemembered():表示用户是通过记住我登录,此时可能不是真正的你(可能是朋友在用你的电脑,也可能何可获取了你的Session)在访问
两者二选一,即subject.isAuthenticated() == true,则subject.isRemembered() == false;反之亦然

建议

在访问一般网页时,认证和记住我验证一个就可以,但是在访问特殊网页的使用,只能认证成功才能访问

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值