1 Shiro 三大核心组件
Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。先来看一下它们之间的关系。
1.1 Subject 为认证主体
包含 Principals 和 Credentials 两个信息。我们看下两者的具体含义。
Principals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份。
Credentials:代表凭证。常见的有密码,数字证书等等。
说白了,两者代表了需要认证的内容,最常见的便是用户名、密码了。比如用户登录时,通过 Shiro 进行身份认证,其中就包括主体认证。
1.2 SecurityManager 为安全管理员
这是 Shiro 架构的核心,是 Shiro 内部所有原件的保护伞。项目中一般都会配置 SecurityManager,开发人员将大部分精力放在了 Subject 认证主体上,与 Subject 交 互背后的安全操作,则由 SecurityManager 来完成。
1.3 Realm 是一个域
它是连接 Shiro 和具体应用的桥梁。当需要与安全数据交互时,比如用户账户、访问控制等,Shiro 将会在一个或多个 Realm 中查找。我们可以把 Realm 看作 DataSource,即安全数据源。一般,我们会自己定制 Realm,下文会详细说明。
2 Shiro 身份和权限认证
2.1 Shiro 身份认证
我们分析下 Shiro 身份认证的过程,首先看一下官方给出的认证图。
Step1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。
Step2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。这里开始了真正的认证工作。
Step3、4、5:SecurityManager 根据具体的 Realm 进行安全认证。从图中可以看出,Realm 可进行自定义(Custom Realm)。
2.2 Shiro 权限认证
权限认证,也就是访问控制,即在应用中控制谁能访问哪些资源。在权限认证中,最核心的三个要素是:权限、角色和用户。
权限(Permission):即操作资源的权利,比如访问某个页面,以及对某个模块的数据进行添加、修改、删除、查看操作的权利。
角色(Role):指的是用户担任的角色,一个角色可以有多个权限。
用户(User):在 Shiro 中,代表访问系统的用户,即上面提到的 Subject 认证主体。
一个用户可以有多个角色,而不同的角色可以有不同的权限,也可有相同的权限。比如说现在有三个角色,1 是普通角色,2 也是普通角色,3 是管理员,角色 1 只能查看信息,角色 2 只能添加信息,管理员对两者皆有权限,而且还可以删除信息。
3 Spring Boot 集成 Shiro
3.1 依赖导入
Spring Boo 集成 Shiro 需要导入如下 starter 依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
3.2准备页面和跳转
@Controller
public class ViewController {
/**
* 跳转到login页面
*/
@RequestMapping("toLogin")
public String toLogin(){
return "login";
}
/**
* 登录校验
* @param username 用户名
* @param password 密码
* @param model
* @return
*/
@RequestMapping("/login")
public String Login(String username, String password, Model model){
//认证主体
Subject subject = SecurityUtils.getSubject();
//把用户封装成一个token
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
return "index";
}catch (UnknownAccountException e) {
model.addAttribute("message","用户不存在");
return "login";
} catch (AuthenticationException e) {
model.addAttribute("message","密码错误");
return "login";
}
}
/**
* 不想准备页面,直接返回字符串
*/
@RequestMapping("/vip/vip1")
@ResponseBody
public String toVip1(){
return "已进入vip1";
}
@RequestMapping("/vip/vip2")
@ResponseBody
public String toVip2(){
return "已进入vip2";
}
}
登录页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>title</title>
<body>
<p th:text="${message}" style="color: red"></p>
<form th:action="@{/login}">
用户名:<input type="text" name="username"><br>
密 码:<input type="text" name="password">
<input type="submit" value="提交">
</form>
</body>
index页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>title</title>
<body>
<h1>这是首页</h1>
<a th:href="@{/vip/vip1}">vip/vip1</a>
<a th:href="@{/vip/vip2}">vip/vip2</a>
</body>
3.3 自定义 Realm
自定义 Realm 需要继承 AuthorizingRealm 类,该类封装了很多方法,且继承自 Realm 类。
继承 AuthorizingRealm 类后,我们需要重写以下两个方法。
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
具体实现如下,相关注解请见代码注释:
public class UserRealm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//可以创建set集合同时添加多个权限
Set<String> perms = new HashSet<String>();
perms.add("user:vip1");
//perms.add("user:vip2");
//给用户添加权限
info.setStringPermissions(perms);
//info.setRoles(); 给用户添加角色
return info;
}
/**
* 认证
* @param token
* @return SimpleAuthenticationInfo
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//可以根据用户名从数据库查询用户,为了方便这里伪造一个用户
String user = "root";
String pw = "password";
//转换成登录时的token
UsernamePasswordToken token1 = (UsernamePasswordToken) token;
if (!token1.getUsername().equals(user)) {
//用户名不正确,自动抛出UnknownAccountException
return null;
}
//密码shiro帮我们验证,错误也会抛出异常
return new SimpleAuthenticationInfo("",pw,"");
}
}
从上面两个方法中可以看出,验证身份时需先根据用户输入的用户名从数据库中查出对应的用户,这时还未涉及到密码,也就是说即使用户输入的密码不正确,照样可以查询出该用户。
然后,将该用户的相关信息封装到 authcInfo 中并返回给 Shiro。接下来就该 Shiro 上场了,将封装的用户信息与用户的输入信息(用户名、密码)进行对比、校验(注意,这里对密码也要进行校验)。校验通过则允许用户登录,否则跳转到指定页面。
同理,权限验证时,也需先根据用户名从数据库中获取其对应的角色和权限,将其封装到 authorizationInfo 并返回给 Shiro。
3.4 Shiro 配置
自定义 Realm 写好了,接下来需要配置 Shiro。我们主要配置三个东西:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器。
首先,配置自定义的 Realm,代码如下:
@Configuration
public class ShiroConfig {
/**
* 获取自定义的Realm
* @return
*/
public UserRealm getUserRealm(){
return new UserRealm();
}
}
接着,配置安全管理器 SecurityManager:
@Configuration
public class ShiroConfig {
/**
* 注入安全管理器
* @return securityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(getUserRealm());
return securityManager;
}
}
配置 SecurityManager 时,需要将上面自定义 Realm 添加进来,这样 Shiro 才可访问该 Realm。
最后,配置 Shiro 过滤器:
@Configuration
public class ShiroConfig {
/**
* 注入 Shiro 过滤器
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
/**
* 常用的几个权限
* anon 所有人都可以通过
* authc 必须认证了才能通过
* perms 拥有该资源权限才能通过
* user 开启记住我功能可以通过
* role 拥有该角色才能通过
*/
Map<String,String> filter = new LinkedHashMap<>();
filter.put("/","anon");
//filter.put("/vip/**","authc");
//访问vip1必须拥有vip1权限才能通过
filter.put("/vip/vip1","perms[user:vip1]");
filter.put("/vip/vip2","perms[user:vip2]");
bean.setFilterChainDefinitionMap(filter);
//设置登录页面
bean.setLoginUrl("/toLogin");
return bean;
}
}
配置 Shiro 过滤器时,我们引入了安全管理器。
Shiro 配置一环套一环,遵循从 Reaml 到 SecurityManager 再到 Filter 的过程。在过滤器中,我们需要定义一个 shiroFactoryBean,然后将 SecurityManager 引入其中,需要配置的内容主要有以下几项。
- 默认登录的 URL---------setLoginUrl()。
- 认证成功之后要跳转的 URL--------setSuccessUrl()。
- 需要拦截或者放行的 URL:这些都放在一个 Map 中--------setFilterChainDefinitionMap()。
Q.E.D.