学会使用Shiro
因为在项目中使用到了springboot+Shiro,然后当时为了理解找了很多资料,现将学到的做出一个总结
一 权限管理
权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问并且只能访问自己被授权的资源
权限管理的两大类别:
- 用户认证
- 用户授权
1.1 用户认证
用户访问系统时,系统要验证用户身份的合法性,只有合法才能访问相应的系统资源
1.1.1 方法
- 用户名密码
- 指纹解锁
- 人脸识别
- 基于证书验证
1.1.2 流程
1.1.3 概念
名称 | 说明 |
---|---|
subject [主体] | 用户,访问资源的时候,系统需要对subject进行身份认证 |
principal [身份信息] | 通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息[可以选择身份证认证,学生证认证] |
credential [凭证信息] | 密码,证书,指纹,人脸 |
主体在进行身份认证时需要提供身份信息和凭证信息
1.2 用户授权
在用户认证通过之后,用户具有资源的访问权限才可以进行访问
1.2.1 流程
1.2.2 概念
权限/许可(permission):subject具有permission访问资源
1.2.3 模型
二 Shiro是什么?
Shiro是apache的一个开源框架,是一个权限管理的框架,实现用户认证,用户授权。属于轻量框架
2.1 Shiro的基本架构
名称 | 说明 | |
---|---|---|
Authentication | 认证 | 身份认证/登录,验证用户是不是用户本人 |
Authorization | 授权 | 访问控制的过程,即确定"谁"访问"什么" |
Session Management | 会话管理 | 即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的 |
Cryptography | 加密 | 保护数据的安全性,如密码加密存储到数据库,而不是明文存储 |
Web Support | Web支持 | 可以非常容易的集成到Web环境 |
Caching | 缓存 | 比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率 |
Concurrency | 多线程应用的并发验证 | 即如在一个线程中开启另一个线程,能把权限自动传播过去 |
Testing | 测试 | 提供测试支持 |
Run As | 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问 | |
Remember me | 记住我 | 一次登录后,下次再来的话不用登录了 |
2.2 shiro如何工作?
名称 | 说明 | |
---|---|---|
Subject | 主体 | 所有的Subject都绑定到SecurityManager,与Subject的交互都会委托给SecurityManager |
SecurityManager | 安全管理器 | 1. 所有与安全有关的操作都会与SecurityManager交互 2. 管理着所有Subject 3. 是Shiro的核心,它负责与后边介绍的其他组件进行交互,相当于SpringMVC中的DispatcherServlet前端控制器 |
Realm | 域 | 1. Shiro从从Realm获取安全数据(如用户、角色、权限) 2. SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法 3. 把Realm看成DataSource,即安全数据源【可以使用JDBC实现或者内存实现等等】 4.Shiro不知道你的用户/权限管理存储在哪,以什么格式存储,所以一般在应用中都需要实现自己的Realm |
- 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager
- 要给Shiro的SecurityManager注入Realm,从而让SecurityManager得到合法的用户及其权限进行判断
- Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入
2.3 Shiro内部看Shiro的架构
名称 | 说明 | |
---|---|---|
Authenticator | 认证器 | 负责主体认证的,是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现,其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了 |
Authrizer | 访问控制器 | 用来决定主体是否有权限进行相应的操作 |
SessionManager | 管理session的生命周期。Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器) | |
SessionDAO | 我们想把Session保存到数据库,那么可以实现自己的SessionDAO | |
CacheManager | 缓存控制器 | 来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能 |
Cryptography | 密码模块 | Shiro提高了一些常见的加密组件用于如密码加密/解密的 |
三 Shiro认证
3.1 使用maven的坐标
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.4.0</version>
</dependency>
3.2 流程
/** 1. user_name是用户输入的用户名, user_password是用户输入的密码 **/
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user_name, user_passsword);
try {
/** 2. shiro帮我们匹配用户名密码什么的,我们只需要把东西传给它,他会根据我们在UserRealm里认证方法设置的来验证,调用subject.login进行验证,验证不通过会抛出AuthenticationException异常,然后自定义返回信息 **/
subject.login(token);
} catch(AuthenticationException e) {
/** 3. 身份验证失败 **/
}
/** 4. 用户退出 **/
subject.login();
Subject.login(token)
进行登录,其会自动委托给Security Manager
SecurityManager
负责真正的身份验证逻辑,它会委托给Authenticator
进行身份验证Authenticator
可能会委托给相应的AuthenticationStrategy
进行多Realm身份验证
,默认ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多Realm身份验证
。Authenticator
会把相应的token
传入Realm
,从Realm
获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
四 Realm
主要是两个方法
- 用户授权
/** PrincipalCollection是一个身份集合 **/ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { }
PrincipalCollection是一个身份集合,因为我们现在就一个Realm,所以直接调用getPrimaryPrincipal得到之前传入的用户名即可;然后根据用户名调用UserService接口获取角色及权限信息
- 用户认证
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { }
- 根据传入的用户名获取User信息。[如果user为空,那么抛出没找到账号异常UnknownAccountException]
- 找到user生成
AuthenticationInfo
信息,交给间接父类AuthenticatingRealm
使用CredentialsMatcher
进行判断密码是否匹配。[不匹配抛出密码错误异常IncorrectCredentialsException,如果密码重试此处太多将抛出超出重试次数异常ExcessiveAttemptsException]。组装SimpleAuthenticationInfo
信息时,要传入:身份信息(用户名),凭据(密文密码),盐(username+salt),CredentialsMatcher
使用盐加密传入的明文密码和此处的密文密码进行匹配
4.1 AuthenticationToken
4.1.1 作用
用于收集用户提交的身份(用户名)及凭据(密码)
4.1.2 说明
public interface AuthenticationToken extends Serializable {
Object getPrincipal();//身份
Object getCredentials();//凭据
}
Shiro提供了一个直接拿来用的UsernamePasswordToken
,用于实现用户名/密码Token组,另外其实现了RememberMeAuthenticationToken
和HostAuthenticationToken
,可以实现记住我及主机验证的支持
- 扩展接口
RememberMeAuthenticationToken
:提供了boolean isRememberMe()
记住我的功能- 扩展接口
HostAuthenticationToken
:提供了String getHost()
用于获取用户"主机"的功能
4.2 AuthenticationInfo
4.2.1 作用
- 如果Realm是AuthenticatingRealm的子类,则提供给AuthenticatingRealm内部使用的CredentialsMatcher进行凭据验证[如果没有继承它需要在自己的Realm中自己实现验证]
- 提供给SecurityManager来创建Subject(提供身份信息)
4.3 PrincipalCollection
4.3.1 作用
因为我们可以在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(); //判断是否为空
}
- 如果只有一个Principal那么直接返回即可,如果有多个Principal,则返回第一个(因为内部使用Map存储,所以可以认为是返回任意一个)
- oneByType / byType根据凭据的类型返回相应的Principal
- fromRealm根据Realm名字(每个Principal都与一个Realm关联)获取相应的Principal
4.4 AuthorizationInfo
4.4.1 作用
用来聚合授权信息的
public interface AuthorizationInfo extends Serializable {
Collection<String> getRoles(); //获取角色字符串信息
Collection<String> getStringPermissions(); //获取权限字符串信息
Collection<Permission> getObjectPermissions(); //获取Permission对象信息
}
五 我项目中使用Shiro+数据库+SpringBoot
5.1 Controller
public Result userLogin(@RequestBody HashMap<Object, Object> map) {
String user_name = (String)(map.get("user_name"));
String user_password = (String)(map.get("user_password"));
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user_name, user_password);
try{
subject.login(token);
} catch (AuthenticatioException e) {
//账号不存在
}
}
5.2 MyRealm
public class MyRealm extends AuthorizingRealm {
@Autowired
private LogMapper logMapper;
/** 用户授权 **/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
}
/** 用户认证 **/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
/** 用户输入的用户名 **/
String user_name = token.getPrincipal().toString();
/** 数据库找的用户名和密码 **/
User user = logMapper.getByUserName(user_name);
if(user != null) {
/** 数据库找的用户名和密码 **/
Object principal = user.getName();
Object credentials = user.getPassword();
ByteSource salt = ByteSource.Util.bytes(user.getName());
/** 当前realm的名称 **/
String realName = this.getName();
//SimpleAuthenticationInfo中有个位置会获取token中的密码与它做对比,如果一样就正常返回,不一样就抛出异常
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
principal,
credentials,
salt,
realmName);
}
}
}
5.3 ShiroConfiguration
@Configuration
public class ShiroConfiguration {
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 密码编码
*/
@Bean(name = "hashCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//加密算法的名称
credentialsMatcher.setHashAlgorithmName("MD5");
//配置加密的次数
credentialsMatcher.setHashIterations(2);
//是否存储为16进制
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean(name = "myRealm")
@DependsOn("lifecycleBeanPostProcessor")
public MyRealm myRealm(){
MyRealm realm = new MyRealm();
//配置自定义密码比较器
realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager(){
return new EhCacheManager();
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
//用户授权/认证信息Cache, 采用EhCache缓存
securityManager.setCacheManager(ehCacheManager());
//注入记住我管理器
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* authc:该过滤器下的页面必须验证后才能访问,他是Shiro内置的一个拦截器
* anno:它对应的过滤器里面是空的,什么都没做,可以理解为不拦截
* authc:所有url都必须认证通过才可以访问
* anno:所有url都可以匿名访问
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
//拦截器。匹配原则是最上面的优先匹配
Map<String, String> filterChainMap = new LinkedHashMap<>();
//配置不会被拦截的链接
// filterChainMap.put("/login", "anon");
// filterChainMap.put("/register", "anon");
// //配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
// filterChainMap.put("/doLogout", "logout");
// //剩余请求需要身份认证
// filterChainMap.put("/**", "authc");
// //如果不设置,默认会自动寻找web工程根目录下的"/login.jsp"页面
// shiroFilterFactoryBean.setLoginUrl("/login");
// //未授权界面
//// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
// shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
/**
* cookie对象
* rememberMeCookie()方法是设置Cookie的生成模板,比如cookie的name,cookie的有效时间等等
*/
@Bean("rememberMeCookie")
public SimpleCookie rememberMeCookie() {
//这个参数是cookie的名称,对应前端checkbox的name=rememberMe
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//记住我cookie生效时间30天,单位秒
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* cookie管理对象
* rememberMeManager()方法是生成rememberMe管理器,并且要将这个rememberMe管理器设置到securityManager中
*/
@Bean("rememberMeManager")
public CookieRememberMeManager rememberMeManager() {
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
//rememberMe cookie加密的密钥,默认AES算法,密钥长度(128 256 512 位)
cookieRememberMeManager.setCipherKey(Base64.decode("2AvVhdsgUs0FSA3SDFAdag=="));
return cookieRememberMeManager;
}
}
用户的subject.login(UsernamePasswordToken)
会自动调用MyRealm中的doGetAuthenticationInfo()
方法,如果从数据库获取的用户信息不为空,则把用户信息传入SimpleAuthenticationInfo
中,Shiro会默认使用shiroConfiguration中的HashedCredentialsMatcher
中的方式,把用户输入的密码生成散列值与数据库的密码作比较。如果相同,则通过校验,否则抛出异常