# (一)概述 {#一-概述}
Shiro是Apache的一个安全框架,Shiro可以非常容易的开发出安全性足够好的应用,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,即安全数据源。
# (二)SpringBoot整合Shiro {#二-springboot整合shiro}
首先引入Shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
通过上面的这些概念,我们开始来写代码,使用Shiro只需要三步
1.创建realm对象,需要自定义
2.创建安全管理器,绑定realm对象
3.创建Shiro过滤工厂,绑定安全管理器
首先第一步,自定义一个realm对象
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
定义一个UserRealm继承AuthorizingRealm 并实现接口方法,两个方法分别代表授权和认证,这里的实现方式和SpringSecurity十分类似。
接下来创建一个ShiroConfig类,来实现shiro的配置
@Configuration
public class ShiroConfig {
//创建realm对象,需要自定义
@Bean(name = "userRealm")
public UserRealm userRealm(){
return new UserRealm();
}
//创建安全管理器
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//绑定realm对象
securityManager.setRealm(userRealm());
return securityManager;
}
//创建Shiro过滤工厂
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
}
# (三) shiro实现登陆拦截 {#三-shiro实现登陆拦截}
在Shiro实现登陆拦截只需要在shiro的过滤工厂配置过滤器即可,为了更加具体的展示效果,我们需要新建四个html页面:index、level1、level2、login
另外需要引入thymeleaf依赖以及spring-boot-starter-web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
index.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><!--引入thymeleaf-->
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<h2 th:text="${msg}"></h2>
<a th:href="@{/level1}">level1</a>
<a th:href="@{/level2}">level2</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登陆页</title>
</head>
<body>
<div>
<p th:text="${errormsg}"></p>
<form action="/checklogin" method="post">
<h2>登陆页</h2>
<input type="text" id="username" name="username" placeholder="username">
<input type="password" id="password" name="password" placeholder="password">
<button type="submit">登陆</button>
</form>
</div>
</body>
</html>
level1、level2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
level1
</body>
</html>
然后编写indexController
@Controller
public class indexController {
@RequestMapping({"/","/index"})
public String index(Model model){
model.addAttribute("msg","hello,shiro");
return "index";
}
@RequestMapping("/level1")
public String level1(Model model){
return "level1";
}
@RequestMapping("/level2")
public String level2(Model model){
return "level2";
}
@RequestMapping(value = "/login",method = RequestMethod.GET)
public String tologin(){
return "login";
}
}
在这些前期工作完成后,我们就可以配置过滤拦截器了,在ShiroConfig类中的ShiroFilterFactoryBean 方法中,添加所需要配置的过滤器,shiro内置了五种过滤器
/**
* anon : 无需认证就可以访问
* authc: 必须认证了才能访问
* user: 必须有 记住我 功能才能访问
* perms: 拥有对某个资源的权限才能访问
* role: 拥有某个角色权限才能访问
*/
在使用时,只要将对应的页面分配不同的过滤器即可
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
Map<String,String> filterMap=new LinkedHashMap<>();
filterMap.put("/index","anon");
filterMap.put("/level1","authc");
filterMap.put("/level2","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置跳转登陆页
bean.setLoginUrl("/login");
return bean;
}
在这段代码中,我们设置index首页无需权限就可以访问,level1何level2需要认证了才能访问,登陆页跳转到login页面。实现的效果为进入index首页无需任何认证,当点击level1和level2标签时判断有无认证,如果没有认证自动跳转到login页面。
# (四)Shiro实现用户认证 {#四-shiro实现用户认证}
shiro的用户认证都放在realm中进行,首先我们需要改造一下controller中对登陆逻辑的处理,我们需要将用户传过来的用户名和密码封装到shiro中,新写一个方法判断登陆情况
@RequestMapping(value = "/checklogin",method = RequestMethod.POST)
public String login(@RequestParam("username") String username,@RequestParam("password") String password,Model model){
//获取当前的用户
Subject subject = SecurityUtils.getSubject();
//用来存放错误信息
String msg="";
//如果未认证
if (!subject.isAuthenticated()){
//将用户名和密码封装到shiro中
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 执行登陆方法
subject.login(token);
}catch (UnknownAccountException e){//用户名不存在
msg=e.getMessage();
}catch (IncorrectCredentialsException e){//密码错误
msg=e.getMessage();
}catch (Exception e){
msg="用户登陆异常";
e.printStackTrace();
}
//如果msg为空,说明没有异常,就返回到主页
if (msg.isEmpty()){
return "redirect:/index";
}else {
model.addAttribute("errormsg",msg);
return "login";
}
}
return "login";
}
在这个登陆控制器中,我们首先通过subject判断用户是否存在,如果不存在就封装用户数据进行login。这个login的动作就需要在realm中去执行。回到UserRealm的认证方法:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取到token
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
//从token中获取到用户名和密码
String username = token.getUsername();
String password = String.valueOf(token.getPassword());
//为了方便,这里不从数据库中获取用户
if (!"root".equals(username)) {
throw new UnknownAccountException("用户不存在");
}else if (!"123456".equals(password)){
throw new IncorrectCredentialsException("密码错误");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,"123456",getName());
return info;
}
在之前的代码中我们将用户名和密码存入了UsernamePasswordToken里,在UserRealm中可以通过authenticationToken获取到,获取到用户名和密码之后,我们可以将用户名和密码与数据库进行对比,并向上抛出不同的异常,这些异常会被e.getMessage()所获取到。
这里需要注意的是这个返回值SimpleAuthenticationInfo ,这个类是AuthenticationInfo 的实现类,SimpleAuthenticationInfo的构造方法需要传入三个参数:
第一个参数是principal,一般会传入用户名或者用户实体类,然后在其他地方通过下面这段代码获取到当前登陆的用户
SecurityUtils.getSubject().getPrincipal();
第二个参数是密码,注意这里是指数据库中的密码,因为我们在前面的代码中已经做了一层密码判断,这里的密码校验就没有多大效果。
第三个参数是Realm的名称,直接使用getName()方法获取即可。
# (4.1)效果展示 {#_4-1-效果展示}
进入首页后点击level1或者level2自动跳转登陆页
进入登陆页后如果用户名输入错误显示用户不存在,密码错误则展示密码错误
当用户名和密码都输入正确后进入首页,此时点击level1或者level2都进入两个页面。
# (五)Shiro实现用户授权 {#五-shiro实现用户授权}
在前面我们实现了用户的认证,接下来我们来做用户授权,用户授权在很多场景下都可以见到,比如一个网站的某些页面只有vip可以见到。
我们来模拟上面的这个场景,首先修改ShiroConfiggetShiroFilterFactoryBean方法,增加权限
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
Map<String,String> filterMap=new LinkedHashMap<>();
//level1只有vip1才能访问
filterMap.put("/level1","perms[vip1]");
filterMap.put("/index","anon");
bean.setFilterChainDefinitionMap(filterMap);
bean.setLoginUrl("/login");
//如果没有权限跳转的页面
bean.setUnauthorizedUrl("/unauthorizedUrl");
return bean;
}
level1只有vip1才能访问,如果没有权限就会跳转到/unauthorizedUrl,我们先把这个跳转后的路径写好:
@RequestMapping("/unauthorizedUrl")
@ResponseBody
public String unAuthorizedUrl(){
return "当前用户没有权限访问";
}
接着在UserRealm中编写授权部分的代码:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//在这里为每一个用户添加vip1权限
info.addStringPermission("vip1");
return info;
}
在这段代码中通过addStringPermission为所有用户增加了vip1权限,在真实的环境中,我们会根据不同的用户给出不同的权限,比如在数据库中增加一个权限字段,上面代码中填写vip1的地方用数据库中的权限字段代替。
# (六)总结 {#六-总结}
至此,我们已经将shiro的引入、基本使用、认证、授权都学完一遍了。Shiro和SpringSecurity各有优点,具体选择用哪个框架,看你们公司的选择吧。