Shiro基操
Shiro简介
Apache Shiro 是 Java 的一个轻量级安全框架。虽说没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个需要根据业务需求来决定 。
Shiro的基本API
上图为Shiro的基本功能模块
- Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率: - Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
注意:Shiro 不会去维护用户、维护权限;需要我们自己去设计 / 提供;然后通过相应的接口注入给 Shiro 。
Shiro的工作原理
可以看到:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject;其每个 API 的含义:
-
Subject:主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者;
-
SecurityManager:安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器;
-
Realm:域,Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个 Shiro 应用:
- 应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;
- 我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro 不提供维护用户 / 权限,而是通过 Realm 让开发人员自己注入
编写一个简单的Realm (未集成SpringBoot)
从上文中可以知道,shiro所提供的安全保障是基于 Realm 所提供的数据支持 , 现在我们来简单配置一个自己的Realm , 本文采用阿里巴巴的 Druid-1.1.22 数据源管理
一、新建Shiro配置信息(shiro.ini)
#声明一个自定义的的realm
customRealm=com.woniuxy.framework.component.MyRealm
#将realm注入到securityManager中
securityManager.realms=$customRealm
创建Realm类 (MyRealm.java)
这里做一个简单的登录验证
//自定义Realm类需要实现Shiro接口 AuthorizingRealm
public class MyRealm extends AuthorizingRealm {
//建立数据源
private static JdbcTemplate jdbcTemplate = null;
static{
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/edu");
dataSource.setUsername("root");
dataSource.setPassword("123321");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
//这是授权验证功能
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
//AuthenticationToken:认证令牌,认证入参
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("进入获取认证信息方法:"+token);
//获取到用户名
Object username = token.getPrincipal();
//在数据库中查询用户是否存在
List<User> userList = jdbcTemplate.query(
"select * from edu_user where username=?", //sql
new BeanPropertyRowMapper<User>(User.class), //结果映射器,将ResultSet中的内容自动映射到User类上
username);//参数
//判断结果
if(userList.size()==0){
throw new UnknownAccountException(username+"用户不存在");
}
if(userList.size()>1){
throw new AccountException("系统异常,找到多个用户"+username);
}
//适配返回对象, 用户、密码、Realm名字
return new SimpleAuthenticationInfo(userList.get(0),userList.get(0).getPassword(),MyRealm.class.getName());
}
}
三、进行测试
//加载配置文件,创建工厂类
IniSecurityManagerFactory iniSecurityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
//创建安全管理器
SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
//将安全管理器设置到SecurityUtils中
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象,代表一个用户
Subject subject = SecurityUtils.getSubject();
//认证
subject.login(new UsernamePasswordToken("username","password"));
System.out.println("确认验证是否通过"+subject.isAuthenticated());
这是Subject源码中关于subject.login的描述 , 如果验证失败则会报错 , 可以通过try…catch处理
集成SpringBoot
需要导入 shiro-spring-boot-starter
本次不考虑实现前后端分离 , 简单的写一个静态登录页面即可
一、修改配置选项
1) 新建一个配置类
@Configuration
public class SecurityConfig {
@Bean
public Realm shiroRealm(){
return new ShiroRealm();
}
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition dsfcd = new DefaultShiroFilterChainDefinition();
//anon为shiro自带的过滤器名称
/**
* anon:无需认证即可访问
* authc:需要认证才可访问
* user:点击“记住我”功能可访问
* perms: 拥有权限才可以访问
* role: 拥有某个角色权限才能访问
*/
dsfcd.addPathDefinition("/","anon");
dsfcd.addPathDefinition("/login","anon");
dsfcd.addPathDefinition("/login.html","anon");
//登出
dsfcd.addPathDefinition("/logout","logout");
//其他则需要认证
dsfcd.addPathDefinition("/**","user");
return dsfcd;
}
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
/**
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole等shiro注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
}
2)在SpringBoot主配置文件中添加(本文使用properties格式文件)
shiro.loginUrl=/login.html
二、实现ShiroRealm
// 将上文Realm内容中的查询语句用Mybatis等重新编写一下即可 , 本文采用Mybatis-plus
public class MyRealm extends AuthorizingRealm {
@Resource
//用户数据调用
private UserMapper userMapper;
@Resource
//角色数据调用
private RoleMapper roleMapper;
@Resource
//权限数据调用
private PermMapper permMapper;
@Override
//这是授权验证功能
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
//AuthenticationToken:认证令牌,认证入参
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("进入获取认证信息方法:"+token);
//获取用户名
Object username = token.getPrincipal();
//获取密码
Wrapper<UserInfo> queryWrapper= new QueryWrapper<UserInfo>()
.eq("user_name",username);
UserInfo userInfo = null;
try{
userInfo= userMapper.selectOne(queryWrapper);
}catch(Exception e){
throw new AccountException("系统异常,找到多个用户"+username);
}
if(userInfo==null){
throw new UnknownAccountException(username+"用户不存在");
}
log.info(">>>>>>>>验证成功!"+userInfo);
return new SimpleAuthenticationInfo(userInfo,userInfo.getUserPassword(),ShiroRealm.class.getName());
}
}
编写Controller层
@RestController
@Slf4j
public class LoginController {
@PostMapping("/login")
public String login(String username,String password){
Subject subject = SecurityUtils.getSubject();
subject.login(new UsernamePasswordToken(username,password));
return "success";
}
@GetMapping("/pay")
public String pay(){
return "success";
}
}
这样前端代码发送请求即可实现验证