当我学完Springboot的基本用法之后,我们可以很快速很熟练的对某某数据库中的某某表进行CRUD(增,删,改,查),虽然CRUD是一个管理系统的主要内容,但是权限验证,访问系统前必须登录,不同用户有不同的权限,注销等操作,也是必不可少的。虽然使用过滤器,拦截器等操作,我们也可以实现上述功能,但是框架已经帮我们封装好了相关的拦截器,和过滤器来实现上述功能,因此掌握 shiro这个框架,就能简单快速的实现上述功能了
什么是Shiro
是一款主流的 Java 安全框架,不依赖任何容器,可以运行在 Java SE和 Java EE 项目中,它的主要作用是对访问系统的用户进行身份认证、授权、会话管理、加密等操作。
身份认证:例如,你只有登录了才能进行访问,判断用户是否是登录状态
(在之前MVC框架中,我们常用的是用过滤器来判断它是否有某个session,没有session的话,就让其登录
授权:例如,你必须拥有某些权限才能去访问某些模块
Shiro 就是用来解决安全管理的系统化框架。
Shiro的核心组件
- UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。
- SecurityManager,Shiro 的核心部分,负责安全认证和授权。
- Suject,Shiro 的一个抽象概念,包含了用户信息。
- Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
- AuthenticationInfo,用户的角色信息集合,认证时使用。
- AuthorzationInfo,角色的权限信息集合,授权时使用。
- DefaultWebSecurityManager,安全管理器,开发者自定义的Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
- ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。
用户,角色,权限 三者之间的关系
一般情况下,我们是给用户赋予角色,,给角色赋予权限,
Shiro 的运行机制如下图所示。
Shiro实现登录验证
结合一个Springboot项目来学习Shiro是如何实现登录验证的
Springboot项目搭建
导入项目的依赖
主要导入的依赖有,mysql(用来连接数据库),mybatispuls(用来简化SQL的一个工具,也可以用JPA来代替,或者直接自己写SQL语句),shiro(本次项目的主角),web依赖相关(web项目来展示效果),Lombook工具
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.southwind</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
进行相关配置相关的配置文件,主要是一下数据库和日志的相关配置。
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8&serverTimezone=GMT%2B8
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
整体项目结构如下:
建立数据库
--
-- Table structure for table `account`
--
DROP TABLE IF EXISTS `account`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `account` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL,
`perms` varchar(20) DEFAULT NULL,
`role` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `account`
--
LOCK TABLES `account` WRITE;
/*!40000 ALTER TABLE `account` DISABLE KEYS */;
INSERT INTO `account` VALUES (1,'zs','123123','',''),(2,'ls','123123','manage','administrator'),(3,'ww','123123','','');
/*!40000 ALTER TABLE `account` ENABLE KEYS */;
UNLOCK TABLES;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
entity实体类,与数据库进行对应
package com.southwind.entity;
import lombok.Data;
@Data
public class Account {
private Integer id;
private String username;
private String password;
private String perms;
private String role;
}
注 :正常情况下,我们是有两个表的,一个是用户表(用户表里面就会说明这个用户是什么角色例如:它是普通用户,还是管理员用户),另外一个表就是权限表(权限表里面会说明,一个角色拥有哪些权限,例如普通用户就只有查看的权限,管理员用户就有增删改查的权限。
这里我将两个表合并了
Dao层(对应的是mapper包),这里直接采用的是mybatisplus,来简化SQL语句的书写,也可以用SpringDataJPA
代码如下:
package com.southwind.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.southwind.entity.Account;
import org.springframework.stereotype.Repository;
@Repository
public interface AccountMapper extends BaseMapper<Account> {
}
Service层
- 先定义个接口
package com.southwind.service;
import com.southwind.entity.Account;
public interface AccountService {
public Account findByUsername(String username);
}
- 再定义接口实现类
package com.southwind.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.southwind.entity.Account;
import com.southwind.mapper.AccountMapper;
import com.southwind.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public Account findByUsername(String username) {
QueryWrapper wrapper = new QueryWrapper();
wrapper.eq("username",username);
return accountMapper.selectOne(wrapper);
}
}
Controller层
package com.southwind.controller;
import com.southwind.entity.Account;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class AccountController {
@GetMapping("/{url}")
public String redirect(@PathVariable("url") String url){
return url;
}
@PostMapping("/login")
public String login(String username, String password, Model model){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
subject.login(token);
Account account = (Account) subject.getPrincipal();
subject.getSession().setAttribute("account",account);
return "index";
} catch (UnknownAccountException e) {
e.printStackTrace();
model.addAttribute("msg","用户名错误!");
return "login";
} catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误!");
e.printStackTrace();
return "login";
}
}
@GetMapping("/unauth")
@ResponseBody
public String unauth(){
return "未授权,无法访问!";
}
@GetMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
}
简单页面展示
定义五个简单的页面
- 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>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<h1>index</h1>
<div th:if="${session.account != null}">
<span th:text="${session.account.username}+'欢迎回来!'"></span><a href="/logout">退出</a>
</div>
<a href="/main">main</a> <br/>
<div shiro:hasPermission="manage">
<a href="manage">manage</a> <br/>
</div>
<div shiro:hasRole="administrator">
<a href="/administrator">administrator</a>
</div>
</body>
</html>
- login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<form action="/login" method="post">
<table>
<span th:text="${msg}" style="color: red"></span>
<tr>
<td>用户名:</td>
<td>
<input type="text" name="username"/>
</td>
</tr>
<tr>
<td>密码:</td>
<td>
<input type="password" name="password"/>
</td>
</tr>
<tr>
<td>
<input type="submit" value="登录"/>
</td>
</tr>
</table>
</form>
</body>
</html>
- main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<h1>main</h1>
</body>
</html>
- manage.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<h1>manage</h1>
</body>
</html>
- administator.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="shortcut icon" href="#"/>
</head>
<body>
<h1>administator</h1>
</body>
</html>
环境测试
启动Springboot项目后,访问localhost:8080即可进入下面的页面
此时,在没有配置shiro之前,所有的链接,如main,manage,administrator,点击后都是可以访问的。
在配置好环境后,shiro主角就要登场了。
Shiro的配置
- Shiro到底要帮我们实现什么呢?
Shiro要帮我们实现
自定义AccountReam
Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
代码如下:
package com.southwind.realm;
import com.southwind.entity.Account;
import com.southwind.service.AccountService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
public class AccoutRealm extends AuthorizingRealm {
@Autowired
private AccountService accountService;
/**
* 授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取当前登录的用户信息
Subject subject = SecurityUtils.getSubject();
Account account = (Account) subject.getPrincipal();
//设置角色
Set<String> roles = new HashSet<>();
roles.add(account.getRole());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//设置权限
info.addStringPermission(account.getPerms());
return info;
}
/**
* 认证
* 即登录相关事件交于这个重写的方法里面
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//这个authenticationToken是哪里来的呢??
//会登录的接口,我们可以发现在登录时,我们封装了一个token
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
Account account = accountService.findByUsername(token.getUsername());
if(account != null){
//当密码验证,即密码错误时,shiro会自己抛出一个密码错误的异常
//getName()这个参数是当前realm的名称
return new SimpleAuthenticationInfo(account,account.getPassword(),getName());
}
//即用户名不存在,shiro框架自己会抛出一个用户不存在异常
return null;
}
}
到这里,reaml就已经写完了,这样它会生效吗??
不,这个它根本不能生效,我们还要把它配置到一个配置类中它才会生效的
写一个配置类
package com.southwind.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.southwind.realm.AccoutRealm;
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.Hashtable;
import java.util.Map;
//@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
//权限设置
Map<String,String> map = new Hashtable<>();
map.put("/main","authc"); // 访问main的时候一定要是登录的一个状态
//下面两个是基于登录的,因为只有在登录之后才可能有权限和角色
map.put("/manage","perms[manage]");//访问manage的时候,一定要有一个manage权限才能去访问
map.put("/administrator","roles[administrator]");//。。。,一定要是administrator角色才可以访问。
factoryBean.setFilterChainDefinitionMap(map);
//设置登录页面
factoryBean.setLoginUrl("/login");
//设置未授权页面
factoryBean.setUnauthorizedUrl("/unauth");
return factoryBean;
}
@Bean
public DefaultWebSecurityManager securityManager(@Qualifier("accoutRealm") AccoutRealm accoutRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(accoutRealm);
return manager;
}
@Bean
public AccoutRealm accoutRealm(){
return new AccoutRealm();
}
//这个是用来整合thymeleaf的
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}
构建流程梳理
除去整合Thymeleaf注入的一个bean,这个配置类中只有3个bean,这3个bean是我们在前文中提到的shiro的核心组件,这个3个组件什么关系呢??
**首先是AccoutRealm ,它是我们自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中,这个逻辑应该放在DefaultWebSecurityManager,应为DefaultWebSecurityManager是负责安全认证和授权的。最后呢,DefaultWebSecurityManager应该放在ShiroFilterFactoryBean中,因为ShiroFilterFactoryBean它是真正处理这里逻辑的底层。
常用的认证和授权规则
- 认证过滤器
anon:无需认证。
authc:必须认证。
authcBasic:需要通过 HTTPBasic 认证。
user:不一定通过认证,只要曾经被 Shiro 记录即可,比如:记住我。 - 授权过滤器
perms:必须拥有某个权限才能访问。
role:必须拥有某个角色才能访问。
port:请求的端口必须是指定值才可以。
rest:请求必须基于 RESTful,POST、PUT、GET、DELETE。
ssl:必须是安全的 URL 请求,协议 HTTPS。
Shiro框架与Spring Security框架
这篇文章,我们可以看到这个Shiro还是比较简单入手的,但是随着微服务和Springboot的发展,Spring Security越来越走向安全框架的中心,下一篇文章一起来学习Spring Security吧