Shiro核心组件
- Subject
主体。Subject在Shiro中是一个接口,接口中定义了认证授权的相关方法。程序通过调用Subject的方法进行认证授权,而Subject使用SecurityManager进行认证授权。
- SecurityManager
权限管理器,它是Shiro的核心。通过SecurityManager可以完成具体的认证、授权等操作,SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager三个接口。
- Authenticator
认证器。对用户登录时进行身份认证
- Authorizer
授权器。用户认证通过后,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
- SessionManager
会话管理。shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上。
- Realm
领域。他是连接数据源+认证功能+授权功能的具体实现。SecurityManager通过Realm获取用户的身份和权限信息,并对用户进行认证和授权。
- SessionDAO
会话dao,是对会话进行操作的一套接口。它可以将session数据存储到数据库或缓存服务器中。
- CacheManager
缓存管理,将用户权限数据存储在缓存中,这样可以减少权限查询次数,提高性能。
- Cryptography
密码管理,Shiro提供了一套加密/解密的组件,方便开发。
Realm
- 数据库认证
@RequestMapping("/user/login")
public String login2(String username,String password){
// 1.获取SecurityManager对象
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 2.为SecurityManager对象设置Realm
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
securityManager.setRealm(jdbcRealm);
// 3.将SecurityManager对象设置到运行环境中
SecurityUtils.setSecurityManager(securityManager);
// 4.获取Subject对象
Subject subject = SecurityUtils.getSubject();
// 5.将前端传来的用户名密码封装为Shiro提供的身份对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 6.shiro认证
subject.login(token);
// 7.认证通过跳转到主页面
return "main";
}catch (AuthenticationException e){
// 8.认证不通过跳转到失败页面
return "fail";
}
}
注意: 使用JDBCRealm数据库表必须叫users,且表中必须要有username和password两列,因为JDBCRealm是通过查询users表对用户进行认证
JDBCRealm源码
将Shiro对象交给容器管理
在上述案例中,所有关于Shiro的对象都是我们自己创建的,我们在SpringBoot中使用Shiro,可以将Shiro的对象交给容器管理简化业务代码。
- 创建Shiro配置类
@Configuration
public class ShiroConfig {
// SecurityManager对象
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(JdbcRealm jdbcRealm){
DefaultWebSecurityManager defaultSecurityManager=new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(jdbcRealm);
return defaultSecurityManager;
}
// JdbcRealm
@Bean
public JdbcRealm getJdbcRealm(DataSource dataSource) {
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
return jdbcRealm;
}
}
自定义Realm
JdbcRealm也可以帮我们完成认证的功能,但是使用JdbcRealm,数据库表名,字段名,认证逻辑都不能变,我们可以通过自定义Realm完成更灵活的认证。
喜欢阅读源码的小伙伴不难发现JdbcRealm继承了AuthorizingRealm,我们自定义Realm时也要继承AuthorizingRealm,在 AuthorizingRealm中具有授权方法,AuthorizingRealm继承了AuthenticatingRealm,AuthenticatingRealm中具有认证方法,我们只要继承了 AuthorizingRealm就可以自定义授权和认证的方法。
JdbcRealm源码
AuthorizingRealm自定义授权方法
AuthenticatingRealm自定义认证方法
package com.xiaoxiao.relam;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaoxiao.domain.Permission;
import com.xiaoxiao.domain.Users;
import com.xiaoxiao.mapper.UsersMapper;
import org.apache.shiro.authc.*;
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 java.util.List;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UsersMapper usersMapper;
// 自定义认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户输入的用户名
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
// 根据用户名查询用户
QueryWrapper<Users> queryWrapper = new QueryWrapper();
queryWrapper.eq("username",username);
Users users = this.usersMapper.selectOne(queryWrapper);
if(users == null){
throw new UnknownAccountException("账户不存在");
}
// 将查询到的用户封装为身份信息
/*
参数一:用户信息
参数二:用户密码
参数三:盐
参数四:Realm名
*/
return new SimpleAuthenticationInfo(users,users.getPassword(),
ByteSource.Util.bytes(users.getSalt()),"myRealm");
}
// 自定以授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("开始授权");
// 拿到用户认证信息
Users users = (Users) principalCollection.getPrimaryPrincipal();
// 查询用户权限
List<Permission> permissionList = this.usersMapper.findById(users.getUid());
// 遍历权限对象,将所有权限交给Shiro管理
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
permissionList.forEach(permission -> {
simpleAuthorizationInfo.addStringPermission(permission.getUrl());
});
return simpleAuthorizationInfo;
}
}
多Realm认证策略
- 在实际开发中我们的认证策略可能不止一种,此时我们就需要多Realm认证策略
package com.xiaoxiao.relam;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xiaoxiao.domain.Admin;
import com.xiaoxiao.mapper.AdminMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
public class MyRealm2 extends AuthorizingRealm {
@Autowired
private AdminMapper adminMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
QueryWrapper<Admin> queryWrapper = new QueryWrapper();
queryWrapper.eq("name",username);
Admin admin = this.adminMapper.selectOne(queryWrapper);
if(admin == null){
throw new UnknownAccountException("账户不存在");
}
return new SimpleAuthenticationInfo(admin,admin.getPassword(),"myRealm2");
}
}
- 为SecurityManager对象设置Realm
@Bean
public DefaultWebSecurityManager securityManager(MyRealm myRealm,MyRealm2 myRealm2,
DefaultWebSessionManager defaultWebSessionManager,
EhCacheManager ehCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> list = new ArrayList();
list.add(myRealm);
list.add(myRealm2);
securityManager.setRealms(list);
return securityManager;
}
多Realm认证策略
如果有多个Realm,怎样才能认证成功,这就是认证策略。认证策略主要使用的是 AuthenticationStrategy接口,这个接口有三个实现类:
- AtLeastOneSuccessfulStrategy(默认):只要有一个Realm验证成功即可,返回所有成功的认证信息
- FirstSuccessfulStrategy:t-node=“block” data-draft-type=“table” data-size=“normal” data-row-style=“normal”>
只要有一个Realm验证成功即可,只返回第一个成功的认证信息,其他的忽略 |
---|
- AllSuccessfulStrategy:
所有Realm验证成功才算成功,如果有一个失败则认证失败 |
---|
// 设置Realm认证策略
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
// 只要有一个Realm验证成功即可,返回所有成功的认证信息
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
@Bean
public DefaultWebSecurityManager securityManager(MyRealm myRealm,MyRealm2 myRealm2,
DefaultWebSessionManager defaultWebSessionManager,
EhCacheManager ehCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置Realm管理者(需要在realm之前)
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> list = new ArrayList();
list.add(myRealm);
// list.add(myRealm2);
securityManager.setRealms(list);
securityManager.setSessionManager(defaultWebSessionManager);
securityManager.setCacheManager(ehCacheManager);
return securityManager;
}
Shiro认证_异常处理
当Shiro认证失败后,会抛出AuthorizationException异常。该异常的子类分别代表不同的认证失败原因,我们可以通过捕捉它们确定认证失败原因。
- DisabledAccountException:账户失效
- ConcurrentAccessException:竞争次数过多
- ExcessiveAttemptsException:尝试次数过多
- UnknownAccountException:用户名不正确
- IncorrectCredentialsException:凭证(密码)不正确
- ExpiredCredentialsException:凭证过期
SHiro认证_散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,适合于对密码进行加密。比如密码admin,产生的散列值是21232f297a57a5a743894a0e4a801fc3,但在md5解密网站很容易的通过散列值得到密码admin。所以在加密时我们可以加一些只有系统知道的干扰数据,这些干扰数据称之为“盐”,并且可以进行多次加密,这样生成的散列值相对来说更难破解。
Shiro支持的散列算法:
Md2Hash、Md5Hash、Sha1Hash、Sha256Hash、Sha384Hash、Sha512Hash
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(5);
return hashedCredentialsMatcher;
}
@Bean
public MyRealm myRealm(HashedCredentialsMatcher hashedCredentialsMatcher){
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return myRealm;
}
Shiro认证_过滤器
在以上案例中,虽然有认证功能,但即使没有登录也可以访问系统资源。如果要配置认证后才能访问资源,就需要使用过滤器拦截请求。Shiro内置了很多过滤器:
- anon:不需要认证即可访问的资源(比如登录页、登录路径、登录失败页面)
- authc:配置登录认证后才可访问的资源(一些安全性比较高的,比如:支付)
- user:配置登录认证或者记住我功能即可访问的资源
// 过滤器
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
// 创建Shiro过滤器工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置SecurityManager
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 设置请求过滤
Map<String,String> map = new HashMap();
// 不需要认证就能访问的资源
map.put("/login.html","anon");
map.put("/user/login2","anon");
map.put("/fail.html","anon");
map.put("/css/**","anon");
// 授权过滤器
map.put("/reportform/find","perms[/reportform/find]");
map.put("/salary/find","perms[/salary/find]");
map.put("/staff/find","perms[/staff/find]");
// 需要认证才能访问的资源
map.put("/user/pay","authc");
map.put("/**","user");
// 为过滤器工厂设置过滤条件
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 设置登录页面
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 设置权限不足的访问的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noPermission.html");
return shiroFilterFactoryBean;
}
Shiro认证_记住我功能
Remember Me为“记住我”功能,即登录成功后,下次访问系统时无需重新登录。当使用“记住我”功能登录后,Shiro会在浏览器Cookie中保存序列化后的认证数据。之后浏览器访问项目时会携带该Cookie数据,这样不登录也可以完成认证。
- 配置Cookie生成器
- 配置记住我管理器
// cookie生成器
@Bean
public SimpleCookie simpleCookie(){
SimpleCookie rememberCookie = new SimpleCookie("rememberCookie");
// cookie保存的时间 单位:s
rememberCookie.setMaxAge(20);
return rememberCookie;
}
// 记住我管理器
@Bean
public CookieRememberMeManager cookieRememberMeManager(SimpleCookie simpleCookie){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(simpleCookie);
// 密钥
cookieRememberMeManager.setCipherKey(Base64.decode("jdikowje9tru908uf9vjhfreoiqehj0g431=-1=023"));
return cookieRememberMeManager;
}