认识并使用Shiro
- 一、对Shiro的基本认知
- 1、Shiro是什么?
- 2、Shiro的核心组件是?
- 2.1 Subject
- 2.2 UsernamePasswordToken
- 2.3 Realm(重点是:AuthorizingRealm用于授权、AuthenticatingRealm用于认证)
- 2.4 SecurityManager
- 2.5 ShiroFilterFactoryBean(重点)
- 二、使用Shiro(通过Spring Boot整合Shiro)
- 0、需求与思路
- 0.1 需求
- 0.2 思路
- 1、通过脚手架快速创建Spring Boot项目
- 2、构造db表,为用户登录做准备
- 3、Spring Boot整合MySQL数据库
- 3.1 添加依赖
- 3.2 实现简单的DAO
- 3.2.1 配置MySQL和mybatis-plus
- 3.2.2 entity
- 3.2.3 mapper
- 3.2.4 单测
- 4、实现Service层
- 5、实现Controller层
- 5.1 用户输入127.0.0.1:8080/main,服务端返回main.html
- 5.1.1 实现
- 5.1.2 效果
- 5.2 同理,编写剩余的html
- 5、重点:如何使用Shiro安全框架实现认证和授权?
- 5.1 思路与实现
- 5.1.1 定义xxxRealm
- 5.1.2 定义ShiroConfig
- 5.1.2.1 ShiroFilterFactoryBean (设置过滤器)
- 5.1.2.2 过滤效果
- 5.1.2.2 不想用官方的login.jsp来登录,怎么办?
- 5.1.3 登录(认证)
- 5.1.3.1 实现
- 5.1.3.1 效果
- 5.1.4 授权
- 5.2 补充:登出
- 5.2.1 修改index.html
- 5.2.2 修改UserContoller.java
- 5.2.3 效果
- 6、Shiro整合Thymeleaf
- 6.1 pom.xml中引入依赖
- 6.2 提供ShiroDialect组件
- 6.3 修改index.html
- 6.4 效果
- 三、[学习资料](https://www.bilibili.com/video/BV16C4y187S9/?p=1&vd_source=4e39b07bacf3530cded08060a2567e22)
一、对Shiro的基本认知
1、Shiro是什么?
- Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. 【Apache Shiro是一个强大且易用的Java安全框架,它用于认证、授权、加密、对话管理。】官方文档
咱目前主要关注如何通过Shiro实现认证、授权和对话管理。
- Spring MVC通过DispachServlet来控制请求(/xxx/yyy)路由到xxxController中的哪个方法。类似地,
Apache Shiro的核心通过Filter来实现。
先将请求拦截,然后判断是否要认证(要的话,就让用户输入用户名和密码)、是否需要权限(有权限才能访问)。
2、Shiro的核心组件是?
2.1 Subject
- Subject是安全领域的抽象概念,暂且可以简单理解为当前用户。
官方文档:实际上,我们想把它称为 “User”,因为这样 “更合理”,但我们决定不这么做:太多应用程序的现有 API 已经有了自己的 User 类/框架,我们不想与它们发生冲突。【但实际上叫Subject,依然会有冲突:)】
- 服务端收到一个请求,xxxController中的对应方法会来处理。
// 当前用户,在整个Shiro框架运作的过程中,通过拿到subject就能拿到用户信息
Subject subject = SecurityUtils.getSubject();
2.2 UsernamePasswordToken
- 见名知意,封装用户的username和password。
subject.login(new UsernamePasswordToken(username, password));
2.3 Realm(重点是:AuthorizingRealm用于授权、AuthenticatingRealm用于认证)
- Shiro安全框架为了实现认证和授权,定义了2个抽象方法:
// AuthorizingRealm是授权Realm
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
......
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);
......
}
// AuthenticatingRealm是认证Realm
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
......
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
}
- 很显然,这就需要用户去定义xxxRealm,并实现这两个抽象方法。
2.4 SecurityManager
- 将自定义的xxxRealm组件交给DefaultWebSecurityManager,从而实现认证和授权。
2.5 ShiroFilterFactoryBean(重点)
- 拦截请求,做一系列处理。例如,某请求要获取xxx.html页面,前提是先登录,那么就跳转到登录页面。
要让Shiro框架运转起来,先要定义出需要的组件,然后按要求组装起来,这就需要ShiroConfig.java。详见:
5、重点:如何使用Shiro安全框架实现认证和授权?
二、使用Shiro(通过Spring Boot整合Shiro)
0、需求与思路
0.1 需求
- 假设我们的网站有4个页面,分别是index.html(首页)、main.html、manage.html、administrator.html
- 并且,我们需要对用户的访问进行限制:
(1)用户必须登录才能访问main.html
(2)用户必须拥有manage权限才能访问manage.html
(3)用户必须拥有administrator角色才能访问administrator.html
0.2 思路
- 首先,用户登录时,会给服务端传递username和password信息。服务端拿到这些信息后,就要校验username和password(和当初注册时,落入db表的username、password进行比对)。这就要用到Shiro提供的认证服务。
- 认证完成后,用户顺利登录咱们的网站,接下来,他会去访问一些页面,而用户能不能访问,就要看用户具有哪些权限以及何种角色。这就要用到Shiro提供的授权服务。例如,授权manage.html页面,要求“用户必须拥有manage权限才能访问manage.html”。
开干吧!
1、通过脚手架快速创建Spring Boot项目
- 添加shiro依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
2、构造db表,为用户登录做准备
这里假设db表里面已经有一些用户信息了,当用户登录时,会将传入的username和password和db表里的信息进行比对。
- 利用IDEA的Database连一下本地的MySQL
- 连上以后,咱先新建一个database:
CREATE DATABASE learn_shiro_mysql;
- 使用该database,并新建User表:
use learn_shiro_mysql;
CREATE TABLE User (
id INT PRIMARY KEY,
username VARCHAR(32),
password VARCHAR(64),
permission VARCHAR(32),
role VARCHAR(32)
);
- 插入3条记录:
INSERT INTO User (id, username, password)
VALUES (1, '张飞', '123');
INSERT INTO User (id, username, password, permission)
VALUES (2, '关羽', '456', 'manage');
INSERT INTO User (id, username, password, permission, role)
VALUES (3, '刘备', '789', 'manage', 'administrato`在这里插入代码片`r');
- 查看db表:
- 这张表的设计是不合理的。原因在于,一个用户可能拥有多个permission,即username和permission不是一对一的关系,不适合放一张表里。
- 但本文重点在于“认识并使用Shiro技术”,所以暂且不在db表的设计上花费功夫。
3、Spring Boot整合MySQL数据库
3.1 添加依赖
<!-- MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
引入了DAO层框架:mybatis-plus
3.2 实现简单的DAO
3.2.1 配置MySQL和mybatis-plus
# 数据库配置
spring:
datasource:
# Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'.
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/learn_shiro_mysql
username: root
password: xxx
# mybatis-plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
learn_shiro_mysql是上面建好的database。
3.2.2 entity
与User表一一对应
@Data
@TableName("User")
public class User {
private Integer id;
private String username;
private String password;
private String permission;
private String role;
}
3.2.3 mapper
通过mybatis-plus进行增删改查
/**
* 不添加@Repository注解,当通过@Autowired注入时:<br>
* <p>
* @Autowired <br>
* private UserMapper userMapper; <br>
* </p>
* IDEA会给userMapper打上红色下划线,提示:Could not autowire. No beans of 'UserMapper' type found. <br>
* 这是因为UserMapper是接口,不能被实例化。但实际上,运行时会生成一个代理对象,代理对象会继承UserMapper接口,所以可以被@Autowired注入。<br>
* 为了消除红色下划线,所以添加@Repository注解。
* @Autowired
* private UserMapper userMapper;
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
}
3.2.4 单测
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelectAllUser() {
userMapper.selectList(null).forEach(System.out::println);
}
}
User(id=1, username=张飞, password=123, permission=null, role=null)
User(id=2, username=关羽, password=456, permission=manage, role=null)
User(id=3, username=刘备, password=789, permission=manage, role=administrator)
4、实现Service层
- 接口
public interface IUserService {
User selectByUsername(String username);
}
- 实现类
@Service
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Override
public @Nullable User selectByUsername(String username) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
return userMapper.selectOne(queryWrapper);
}
}
- 单测
@SpringBootTest
public class UserServiceImplTest {
@Autowired
private IUserService userService;
@Test
public void testSelectByUsername() {
String username = "刘备";
System.out.println(userService.selectByUsername(username));
}
}
User(id=3, username=刘备, password=789, permission=manage, role=administrator)
5、实现Controller层
5.1 用户输入127.0.0.1:8080/main,服务端返回main.html
5.1.1 实现
@Controller
public class UserController {
@GetMapping("/{url}")
public String redirect(@PathVariable String url) {
// 例如,如果url的值为"main",就返回"main"视图
return url;
}
}
- 在Spring MVC中,如果一个xxxController方法返回一个字符串,那么这个字符串通常被解析为一个视图名(View Name),Spring MVC会基于这个视图名来选择相应的视图(例如一个JSP页面或者一个HTML页面)进行渲染。
- 在Spring Boot项目的"src/main/resources/templates"目录下创建一个"main.html"文件。这个文件就是要返回的Thymeleaf模板。
- 当访问"/main"时,Spring MVC就会找到并渲染名为"main"的Thymeleaf模板文件(即"main.html"),并用它来渲染响应。
5.1.2 效果
- 服务端有异常:
org.thymeleaf.exceptions.TemplateInputException: Error resolving template [favicon.ico], template might not exist or might not be accessible by any of the configured Template Resolvers
- 解决办法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
</head>
<body>
<h1>I am main.html !</h1>
</body>
</html>
如上所述,补上
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
参考资料
5.2 同理,编写剩余的html
- manage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
</head>
<body>
<h1>I am manage.html !</h1>
</body>
</html>
- administrator.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
</head>
<body>
<h1>I am administrator.html !</h1>
</body>
</html>
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
</head>
<body>
<h1>I am index.html !</h1>
<a href="/main">main</a> | <a href="/manage">manage</a> | <a href="/administrator">administrator</a>
</body>
</html>
每个html中都要补上
<link rel="shortcut icon" href="../resources/favicon.ico" th:href="@{/static/favicon.ico}"/>
,着实很离谱。
// 其实还可以这么做:
@GetMapping("favicon.ico")
@ResponseBody
void favicon() {
// 方法体为空即可
}
当然了,正常情况下, 是会提供resources/favicon.ico。所以,不如去网上找一个favicon.ico。
5、重点:如何使用Shiro安全框架实现认证和授权?
目前,用户没有登录,便可以访问上述所有html,这显然是不安全的。因此,我们要使用Shiro技术来认证和授权。
5.1 思路与实现
- 我理解,任何框架的使用思路都是,主流程交给框架,配置交给开发者。
5.1.1 定义xxxRealm
- Shiro安全框架为了实现认证和授权,定义了2个抽象方法:
// AuthorizingRealm是授权Realm
public abstract class AuthorizingRealm extends AuthenticatingRealm implements Authorizer, Initializable, PermissionResolverAware, RolePermissionResolverAware {
......
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection var1);
......
}
// AuthenticatingRealm是认证Realm
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
......
protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
}
- 很显然,这就需要用户去定义xxxRealm,并实现这两个抽象方法。
public class UserRealm extends AuthorizingRealm {
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 暂时 return null;
return null;
}
/**
* 认证,即校验用户的用户名和密码
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 暂时 return null;
return null;
}
}
- 定义好了UserRealm,Shiro怎么感知到呢?–> 当然是配置UserRealm啊,有Spring的帮助下,我们靠ShiroConfig来配置。
5.1.2 定义ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(userRealm);
return defaultWebSecurityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
}
5.1.2.1 ShiroFilterFactoryBean (设置过滤器)
- 满足需求的代码:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 设置过滤器
Map<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/main", "authc");
filterChainDefinitionMap.put("/manage", "perms[manage]");
filterChainDefinitionMap.put("/administrator", "role[administrator]");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
-
理论补充:
(1)认证过滤器:
1)anon
: 无需认证
2)authc
: 必须认证
3)authcBasic
: 需要通过HTTP Basic认证
4)user
: 只要曾经被Shiro记录即可,比如:记住我(2)授权过滤器
1)perms
: 必须拥有某个权限才能访问
2)roles
: 必须拥有某个角色才能访问
3)port
: 请求的端口必须是指定值才可以
4)rest
: 请求必须基于RESTful (POST、PUT、GET、DELETE)
5)ssl
: 必须是安全的URL请求(协议HTTPS)
5.1.2.2 过滤效果
不登录,就没法看有限制的页面了!
5.1.2.2 不想用官方的login.jsp来登录,怎么办?
- 设置登录页面
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
......
// 设置登录页面
shiroFilterFactoryBean.setLoginUrl("/login");
......
}
- 提供login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="text" name="username" placeholder="username">
<input type="password" name="password" placeholder="password">
<input type="submit" value="login">
</form>
</body>
</html>
- 效果:
5.1.3 登录(认证)
5.1.3.1 实现
- Controller层
@PostMapping("/login")
public String login(String username, String password, Model model) {
// 将用户登录的认证过程交给Shiro
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
return "index";
} catch (UnknownAccountException | IncorrectCredentialsException e) {
// 重新返回登录页面
model.addAttribute("error", "用户名不存在 或者 密码错误");
return "login";
} catch (Exception e) {
// 重新返回登录页面
model.addAttribute("error", "登录出错");
return "login";
}
}
使用Model对象将错误信息传递给前端显示。Model是Spring MVC中一个非常重要的对象,它封装了视图显示所需要的数据,在视图中可以很方便的取出Model中的数据。
- 完善“5.1.1 定义xxxRealm”中的UserRealm类的doGetAuthenticationInfo方法
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (authenticationToken instanceof UsernamePasswordToken) {
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
User user = userService.selectByUsername(upToken.getUsername());
if (user != null) {
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
}
return null;
}
当执行
subject.login(new UsernamePasswordToken(username, password));
时,会执行到doGetAuthenticationInfo方法中。
根据用户传入的username去查User表,
(1)没查到,返回null
(2)查到了,但用户传入的密码和User表中的密码不匹配,抛异常
(3)查到了,密码也匹配上了,登录成功
5.1.3.1 效果
- 登录失败
- 登录成功后,点击“manage”
接下来,说明还未授权,接下来就要准备授权了。
- 这里先做一下优化,当未授权时,返回一个用户易读的信息。
(1)ShiroConfig.java
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
......
// 设置未授权页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
(2)UserController
@GetMapping("/unauthorized")
@ResponseBody
public String unauthorized() {
return "您没有权限访问该页面!";
}
(3)效果
5.1.4 授权
- 实现:
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
Subject currentUser = SecurityUtils.getSubject();
Object tmpUser = currentUser.getPrincipal();
if (tmpUser instanceof User) {
User user = (User) tmpUser;
// 角色
Set<String> roles = new HashSet<>();
roles.add(user.getRole());
// 权限
Set<String> perms = new HashSet<>();
perms.add(user.getPermission());
SimpleAuthorizationInfo saInfo = new SimpleAuthorizationInfo();
saInfo.setRoles(roles);
saInfo.setStringPermissions(perms);
return saInfo;
} else {
return null;
}
}
5.2 补充:登出
(1)登录时,我需要知道当前页面是谁在访问,此需求可以简化为:登录后,显示“欢迎xxx回家~”
(2)然后,要提供登出的按钮。
5.2.1 修改index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>I am index.html !</h1>
<div th:if="${session.user != null}">
<p>Welcome, <span th:text="${session.user.username}"></span>!</p>
</div>
<a href="/main">main</a> | <a href="/manage">manage</a> | <a href="/administrator">administrator</a>
<div>
<a href="/logout">logout</a>
</div>
</body>
</html>
IDEA会在
user
下标注黄色波浪线,看着真难受啊~
5.2.2 修改UserContoller.java
@PostMapping("/login")
public String login(String username, String password, Model model) {
// 将用户登录的认证过程交给Shiro
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new UsernamePasswordToken(username, password));
subject.getSession().setAttribute("user", subject.getPrincipal());
return "index";
} catch (UnknownAccountException | IncorrectCredentialsException e) {
...
} catch (Exception e) {
...
}
}
@GetMapping("/logout")
public String logout() {
// 退出登录
SecurityUtils.getSubject().logout();
return "login";
}
(1)当执行
subject.login(new UsernamePasswordToken(username, password));
时,会执行UserRealm类的doGetAuthenticationInfo方法,如果登录成功,那么return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
此时,user赋予了principal。
(2)因此subject.getPrincipal()取出的便是user。
5.2.3 效果
6、Shiro整合Thymeleaf
刘备拥有main.html、manage.html、administrator.html的权限,所以可以看到3个main、manage、administrator的链接,那么对于没有权限的人,咱希望他不要看到对应的链接。
6.1 pom.xml中引入依赖
<!-- Thymeleaf整合Shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
6.2 提供ShiroDialect组件
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
6.3 修改index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>I am index.html !</h1>
<div th:if="${session.user != null}">
<p>Welcome, <span th:text="${session.user.username}"></span>! <a href="/logout">logout</a></p>
</div>
<a href="/main">main</a>
<div shiro:hasPermission="manage">
<a href="/manage">manage</a>
</div>
<div shiro:hasRole="administrator">
<a href="/administrator">administrator</a>
</div>
</body>
</html>
6.4 效果
三、学习资料
讲得真不错~ 感谢老师~