1.Shiro框架简介
Apache Shiro是一个强大且易用的权限管理框架,执行身份验证、授权、密码管理和会话管理等
1-1.Shiro 核心组件:
- UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息创建令牌 Token,登录的过程即 Shiro 验证令牌是否具有合法身份以及相关权限。
- SecurityManager,Shiro 的核心部分,负责安全认证与授权。
- Subject,Shiro 的一个抽象概念,包含了用户信息。
- Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑在 Realm 中实现。
- AuthenticationInfo,用户的角色信息集合,认证时使用。
- AuthorizationInfo,角色的权限信息集合,授权时使用。
- DefaultWebSecurityManager,安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
- ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建一个个 Filter 对象来完成。
1-2.Apache Shiro 认证流程:
- Subject:即当前用户,在权限管理的应用程序里往往需要知道谁能够操作什么,谁拥有操作该程序的权利,shiro中则需要通过Subject来提供基础的当前用户信息,Subject 不仅仅代表某个用户,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。Subject自己不会实现相应的身份验证/授权逻辑,而是通过DelegatingSubject委托给SecurityManager实现。
- SecurityManager:即所有Subject的管理者,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务.
- Realms:Realms则是用户的信息认证器和用户的权限认证器,我们需要自己来实现Realms来自定义的管理我们自己系统内部的权限规则。
- authenticator:认证器,主体进行认证最终通过authenticator进行。
2.搭建springBoot环境——前后的未分离
具体请参考:springBoot整合mybatis带日志输出
本次用到的数据库表:test_shiro.sql
2-1 . 使用idea创建项目时添加web、thymeleaf、mybatis-framewoek、MySQL-driver依赖,注意:这里使用的不是最新版的springBoot,使用的是Spring Boot 1.5.4.RELEASE和Shiro1.4.0
还需导入这些依赖,否侧运行异常:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>
接下来导入spring整合shiro的依赖和Shiro 整合 Thymeleaf的依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>1.2.1</version>
</dependency>
2-2.编写mapper层及mapper映射文件
mapper接口:
@Mapper
public interface UserMapper {
//根据用户名查询用户
User findUserByName(@Param("name") String name);
//根据用户id查询用户的权限
List<String> findPermsById(@Param("userId") Integer userId);
}
mapper映射文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.shiro.mapper.UserMapper">
<!--根据用户名查询用户相关信息-->
<select id="findUserByName" resultType="cn.shiro.pojo.User">
select *
from user
where username = #{name}
</select>
<!--根据用户id查询用户的权限-->
<select id="findPermsById" resultType="java.lang.String">
SELECT `permission`
FROM `user`
INNER JOIN `user_role`
ON `user`.`user_id` = `user_role`.`user_id`
INNER JOIN `role`
ON `user_role`.`role_id` = `role`.`role_id`
INNER JOIN `role_perssion`
ON `role`.`role_id` = `role_perssion`.`role_id`
INNER JOIN `permission`
ON `role_perssion`.`perssion_id` = `permission`.`permission_id`
WHERE `user`.`user_id` = #{userId};
</select>
</mapper>
2-3.编写service及serviceImpl
service接口:
package cn.shiro.service;
import cn.shiro.pojo.User;
import java.util.List;
public interface UserService {
//根据用户名查询用户信息
User findUserByName(String name);
//查询用户权限
List<String> findPermsById(Integer userId);
}
serviceImpl:
package cn.shiro.service.impl;
import cn.shiro.mapper.UserMapper;
import cn.shiro.pojo.User;
import cn.shiro.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper um;
@Override
public User findUserByName(String name) {
return um.findUserByName(name);
}
@Override
public List<String> findPermsById(Integer userId) {
return um.findPermsById(userId);
}
}
2-4.自定义Realm类——调用service
继承AuthorizingRealm,然后实现getAuthenticationInfo和getAuthorizationInfo方法,来完成登录和权限的验证逻辑:
package cn.shiro.shiros;
import cn.shiro.pojo.User;
import cn.shiro.service.UserService;
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 UserRealm extends AuthorizingRealm {
//注入service
@Autowired
private UserService us;
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//客户端传来的 username 和 password 会自动封装到 authenticationToken
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//根据 username 和 password调用service进行查询
User user = us.findUserByName(token.getUsername());
//判断根据用户名和密码查询的用户是否存在
if (user != null) {
//如果返回不为 null,则表示用户名正确,再验证密码,直接返回 SimpleAuthenticationInfo 对象即可
return new SimpleAuthenticationInfo(user, user.getPassword(),user.getUsername());
}
//不存在返回null将由安全管理器处理
return null;
}
}
2-5.编写Shiro配置类(*)
自定义过滤器创建完成之后,需要进行配置才能生效,在 Spring Boot 应用中,不需要任何的 XML 配置,直接通过配置类进行装配
- 该类是Spring为Shiro框架提供的一个整合类
- 作用:对前端请求进行过滤,判断该请求是否需要登录/权限才可以访问
这个配置类中一共自动装配了 4个 bean 实例
- 最后一个@Bean(name="userRealm")用于将上方我们编写的Realm类定义在容器中,由下一个方使用,所有的验证和授权逻辑全部定义在这个 bean 中。
- 第二个@Bean(name="securityManager"),并且将 MyRealm 注入到 DefaultWebSecurityManager bean 中,完成注册。
- 第一个 bean ShiroFilterFactoryBean,这是 Shiro 自带的一个 Filter 工厂实例,所有的认证和授权判断都是由这个 bean 生成的 Filter 对象来完成的,这就是 Shiro 框架的运行机制,开发者只需要定义规则,进行配置,具体的执行者全部由 Shiro 自己创建的 Filter 来完成,它可以实现对哪些请求或访问进行验证与授权
package cn.shiro.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import cn.shiro.shiros.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
//shiro的配置类
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
//创建过滤器工厂
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//配置安全管理器
//将注入的securityManager()配置到过滤器工厂中
shiroFilterFactoryBean.setSecurityManager(securityManager);
//下面配置认证与授权规则:
/*
认证过滤器:
anon:无需认证即可访问,游客身份。
authc:必须认证(登录)才能访问。
authcBasic:需要通过 httpBasic 认证。
user:不一定已通过认证,只要是曾经被 Shiro 记住过登录状态的用户就可以正常发起请求,比如 rememberMe。
授权过滤器:
perms:必须拥有对某个资源的访问权限(授权)才能访问。
roles:必须拥有某个角色权限才能访问。
port:请求的端口必须为指定值才可以访问。
rest:请求必须是 RESTful,method 为 post、get、delete、put。
ssl:必须是安全的 URL 请求,协议为 HTTPS。
*/
Map<String, String> filterMap = new LinkedHashMap<>();
//认证:访问index必须要认证
filterMap.put("/index", "authc");
//认证:这里需要对login进行放行,否则表单无法提交到controller
filterMap.put("/login", "anon");
//认证:设置所有请求要需要验证
//filterMap.put("/*", "authc");
//授权:访问add操作页面需要user:add权限
filterMap.put("/add", "perms[user:add]");
//授权:访问update操作页面需要user:update权限
filterMap.put("/update", "perms[user:update]");
//没有通过认证需要返回login页面进行登录验证
shiroFilterFactoryBean.setLoginUrl("/toLogin");
//没有权限将跳转未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/toNoAuth");
//将上方配置好的认证与授权规则添加到过滤器中
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
/**
* 创建DefaultWebSecurityManager
* 安全管理器,相当于相当于SpringMVC中的DispatcherServlet
* 所有的交互都通过SecurityManager进行控制,它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
*/
@Bean(name = "securityManager") //由上方的方法参数中注入
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联realm
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* 创建Realm(用于跟数据库交互,查询用户是否有权限等)
*/
@Bean(name = "userRealm") //由上方的方法参数注入
public UserRealm getRealm() {
return new UserRealm();
}
@Bean
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
}
2-6.编写controller层
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class TestController {
//测试
@RequestMapping("/index")
public String index(Model model){
model.addAttribute("name","张三");
return "test";
}
//跳转登录页面
@GetMapping("/toLogin")
public String toLogin(){
return "login";
}
//跳转未授权页面
@GetMapping("/toNoAuth")
public String toNoAuth(){
return "noAuth";
}
//用户登录
@PostMapping("/login")
public String update(String name,String password,Model model){
//使用Shiro编写认证操作,步骤如下:
//1.使用Shiro提供的工具类获取Subject(可以理解为一个用户)
Subject subject = SecurityUtils.getSubject();
//2.封装用户的数据 用户名和密码 到token中
UsernamePasswordToken token = new UsernamePasswordToken(name, password);
//3.执行登录方法,底层思路,只要subject.login(token)方法执行没有异常则认为是登陆成功
try {
subject.login(token);
//登录成功
//跳转到test.html 测试页面
return "redirect:/index";
} catch (UnknownAccountException e) {
//e.printStackTrace();
//登录失败:用户名不存在
model.addAttribute("msg", "用户名或密码错误");
return "login";
}catch (IncorrectCredentialsException e) {
//e.printStackTrace();
//登录失败:密码错误
model.addAttribute("msg", "密码错误");
return "login";
}
}
}
2-7.在resources/templates下编写相关页面,作为测试使用
如图:
login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>用户登录</title>
</head>
<body>
<h1>登录页面</h1>
<form method="post" action="login">
用户名:<input type="text" name="name"/><br/>
密码:<input type="password" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
<h3 th:text="${msg}"></h3>
</body>
</html>
noAuth.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>无权限</title>
</head>
<body>
<h1>您没有权限访问该页面</h1>
</body>
</html>
test.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h3 th:text="${name}"></h3>
进入用户添加功能: <a href="add">用户添加</a><br/>
进入用户更新功能: <a href="update">用户更新</a><br/>
<a href="/logout">登出</a>
</body>
</html>
add.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1>用户添加</h1>
</body>
</html>
uodate.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1>用户修改</h1>
</body>
</html>
2-8.启动项目
首先测试直接访问index,看请求是否被拦截:
回车后,会跳转到login页面,并没有直接跳转test页面,说明认证是生效的,因为我们并没有登录,所以无法直接访问test页面
接下来进行登录,故意输错用户名或密码,目的是为了测试TestController中的用户登录方法是否正常执行:
接下来根据提供的数据库表中存在的用户进行登录,是否登录成功:
到此说明我们的认证和登录验证是没问题的!
2-9.接下来就完善授权问题
2-9-1:实现UserRealm中的授权方法
授权逻辑:通过shiro提供的SecurityUtils工具类获取当前登录的用户,然后通过该用户的id查询当前这个用户有哪些权限,关于具体的mapper与service如何实现的上方代码已写好,不在描述,最后将查询到的权限封装到授权中,返回授权对象,具体代码如下:
//授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取当前登录用户
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
//根据当前用户的id查询该用户有哪些权限
List<String> perms = us.findPermsById(user.getUserId());
//打印查询到的权限
System.out.println(perms);
//传入权限
info.addStringPermissions(perms);
return info;
}
2-9-2:在配置类我们已经对访问/add和/update进行的授权规则,只允许有user:add和user:update权限的用户进行访问:
2-9-3:启动项目,访问相应的操作页面
这里根据登录不同的用户进行测试访问,有权限的可以访问/add和/update,无权限的会跳转提示页面,同时控制台也会打印出不同用户持有的权限:
这里很明显,只有zhangsan是没有添加和更新的权力
2-10.最后实现登出功能
2-10-1.在配置类中添加一个Bean
@Bean // 该bean用于登出时调用
public ShiroDialect getShiroDialect() {
return new ShiroDialect();
}
2-10-2.在controller中添加对应的请求方法
//登出
@GetMapping("/logout")
public String logout() {
//获取当前登录用户的对象
Subject subject = SecurityUtils.getSubject();
//退出时将当前登录的对象信息进行清空
subject.logout();
return "login";
}
退出之后再直接访问localhost:8081/index是访问不进去的,会跳转登录界面进行验证登录