基础概念
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
三个核心组件:Subject、 SecurityManager 、 Realms.
- Subject:
即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。 - SecurityManager:
它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。 - Realm:
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
环境搭建
项目配置
创建一个springboot项目,完整的pom文件:
<?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.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.sjh</groupId>
<artifactId>shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- 工具包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
</dependency>
<!-- spring工具包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!-- jsp -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!-- servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<!-- jstl -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后还需要在application.prpperties中配置数据库的信息
# 数据库的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql:///test?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=sjh2019
测试启动成功
创建数据库
权限表:
CREATE TABLE permission(
pid INT(11) PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(255) NOT NULL DEFAULT '',
url VARCHAR(255) DEFAULT ''
)ENGINE=INNODB DEFAULT CHARSET= utf8;
INSERT INTO permission VALUES (1,'add','');
INSERT INTO permission VALUES (2,'del','');
INSERT INTO permission VALUES (3,'update','');
INSERT INTO permission VALUES (4,'query','');
用户表:
CREATE TABLE USER(
uid INT(11) PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(255) NOT NULL DEFAULT '',
PASSWORD VARCHAR(255) NOT NULL DEFAULT ''
)ENGINE=INNODB DEFAULT CHARSET= utf8;
INSERT INTO USER VALUES (1,'kobe','123');
INSERT INTO USER VALUES (2,'james','123');
角色表:
CREATE TABLE role(r
rid INT(11) PRIMARY KEY AUTO_INCREMENT,
rname VARCHAR(255) NOT NULL DEFAULT ''
)ENGINE=INNODB DEFAULT CHARSET= utf8;
INSERT INTO role VALUES (1,'admin');
INSERT INTO role VALUES (2,'customer');
权限-角色表:
CREATE TABLE permission_role(
rid INT(11) NOT NULL,
pid INT(11) NOT NULL,
KEY idx_rid(rid),
KEY idx_pid(pid)
)ENGINE=INNODB DEFAULT CHARSET= utf8;
INSERT INTO permission_role VALUES (1,1);
INSERT INTO permission_role VALUES (1,2);
INSERT INTO permission_role VALUES (1,3);
INSERT INTO permission_role VALUES (1,4);
INSERT INTO permission_role VALUES (2,1);
INSERT INTO permission_role VALUES (2,4);
用户-角色表:
CREATE TABLE user_role(
uid INT(11) NOT NULL,
rid INT(11) NOT NULL,
KEY idx_uid(uid),
KEY idx_rid(rid)
)ENGINE=INNODB DEFAULT CHARSET= utf8;
INSERT INTO user_role VALUES (1,1);
INSERT INTO user_role VALUES (2,2);
创建实体类
权限实体类
public class Permission {
private Integer pid;
private String name;
private String url;
public Integer getPid() {
return pid;
}
public void setPid(Integer pid) {
this.pid = pid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
@Override
public String toString() {
return "Permission{" +
"pid=" + pid +
", name='" + name + '\'' +
", url='" + url + '\'' +
'}';
}
}
角色实体类
public class Role {
private Integer rid;
private String rname;
private Set<Permission> permissionSet=new HashSet<>();
private Set<User> userSet=new HashSet<>();
public Integer getRid() {
return rid;
}
public void setRid(Integer rid) {
this.rid = rid;
}
public String getRname() {
return rname;
}
public void setRname(String name) {
this.rname = rname;
}
public Set<Permission> getPermissionSet() {
return permissionSet;
}
public void setPermissionSet(Set<Permission> permissionSet) {
this.permissionSet = permissionSet;
}
public Set<User> getUserSet() {
return userSet;
}
public void setUserSet(Set<User> userSet) {
this.userSet = userSet;
}
@Override
public String toString() {
return "Role{" +
"rid=" + rid +
", rname='" + rname + '\'' +
", permissionSet=" + permissionSet +
", userSet=" + userSet +
'}';
}
}
用户实体类
public class User {
private Integer uid;
private String username;
private String password;
private Set<Role> roleSet=new HashSet<>();
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoleSet() {
return roleSet;
}
public void setRoleSet(Set<Role> roleSet) {
this.roleSet = roleSet;
}
@Override
public String toString() {
return "User{" +
"uid=" + uid +
", username='" + username + '\'' +
", password='" + password + '\'' +
", roleSet=" + roleSet +
'}';
}
}
创建操作数据库的接口
@Mapper//标明是mapper类,会被spring自动扫描
public interface UserMapper {
//根据用户名查询用户
User findByUsername(@Param("username") String username);
}
配置接口和mybatis
在resources目录下创建mapper目录,起名为UserMapper.xml(目录和文件名要和之前的对应)
<?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="com.sjh.shiro.mapper.UserMapper">
<select id="findByUsername" parameterType="string" resultType="user">
select * from user where username=#{username};
</select>
</mapper>
同时在application.properties中加入mybatis配置
# mybatis
mybatis.type-aliases-package=com.sjh.shiro.model
mybatis.mapper-locations=classpath:mapper/*.xml
创建业务层接口和实现类
接口:
public interface UserService {
User findByUsername(String username);
}
实现类:
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;//波浪线报错不要紧,运行时注入
@Override
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
}
测试
找到src/test包下的测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class ShiroApplicationTests {
@Autowired
private UserService userService;
@Test
public void contextLoads() {
User user = userService.findByUsername("kobe");
System.out.println(user);
}
}
运行测试方法,测试成功
基本案例:根据正确的用户名和密码登陆
自定义认证和授权策略
需要继承AuthorizingRealm
类
//自定义认证授权器
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//认证登陆
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//AuthenticationToken用于存储前端传来的登录信息,强转为UsernamePasswordToken以获取登陆信息的用户名和密码
UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)authenticationToken;
//获取用户名
String username = usernamePasswordToken.getUsername();
//根据用户名从数据库查询
User user = userService.findByUsername(username);
//创建一个认证信息类
//第一个参数 从数据库查询得到的用户对象
//第二个参数 数据库中的密码,会与token中登陆信息的密码比较,匹配后通过
//第三个参数 当前realm的名字
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
}
//授权(认证成功后)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//1 根据realm的名字获取对应realm(获取当前realm)
Collection collection = principalCollection.fromRealm(this.getClass().getName());
//2 获取认证后存储的User对象
User user = (User) collection.iterator().next();
//3 获取当前所有角色
Set<Role> roleSet = user.getRoleSet();
//4 遍历角色集合获取权限,并存储到权限集合
List<String> permissionList=new ArrayList<>();
if(!roleSet.isEmpty()){//4.1 遍历角色集合
for(Role role:roleSet){
Set<Permission> permissionSet = role.getPermissionSet();//4.2 获取每个角色中的权限集合
if(!permissionSet.isEmpty()){//4.3 遍历权限集合
for(Permission permission:permissionSet){
permissionList.add(permission.getName());//将权限名存入集合
}
}
}
}
//5 创建一个授权信息类,将权限名集合添加到其中
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.addStringPermissions(permissionList);
return info;
}
}
自定义密码匹配器
需要继承SimpleCredentialsMatcher
public class CredentialMatcher extends SimpleCredentialsMatcher {
//重写密码匹配方法
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//获取token登陆信息中的密码
UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)token;
String password = new String(usernamePasswordToken.getPassword());
//获取数据库中的密码,即传来的AuthenticationInfo认证信息中的数据库密码
String dbPassword= (String) info.getCredentials();
//返回两者是否相同的结果
return this.equals(password,dbPassword);
}
}
配置shiro
//shiro配置类
@Configuration//注明是配置类
public class ShiroConfig{
@Bean("credentialMatcher")//自定义密码匹配器
public CredentialMatcher getCredentialMatcher(){
return new CredentialMatcher();
}
@Bean("authRealm")//自定义认证验证器
public AuthRealm getAuthRealm(@Qualifier("credentialMatcher") CredentialMatcher credentialMatcher){
//自定义认证授权器
AuthRealm authRealm=new AuthRealm();
//将自定义的密码匹配器传入
authRealm.setCredentialsMatcher(credentialMatcher);
return authRealm;
}
@Bean("securityManager")//自定义web安全管理器
public SecurityManager getSecurityManager(@Qualifier("authRealm") AuthRealm authRealm){
//将自定义认证授权器传入默认web安全管理器
DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
@Bean("shiroFilter")//自定义shiro过滤器
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){
//将自定义web安全管理器传入shiro过滤器
ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
//设置登陆url
shiroFilter.setLoginUrl("/login");
//设置登陆成功url
shiroFilter.setSuccessUrl("/index");
//设置登陆失败url
shiroFilter.setUnauthorizedUrl("/unAuthorized");
//配置拦截方式,第一个参数是请求路径,第二个参数是使用什么拦截器
LinkedHashMap<String,String> filterChainDefinitionMap=new LinkedHashMap<>();
filterChainDefinitionMap.put("/index","authc");//访问首页需要认证
filterChainDefinitionMap.put("/login","anon");//登陆不需要认证,可匿名
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor(@Qualifier("securityManager")SecurityManager securityManager){
//配置spring使用自定义web安全管理器
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator proxyCreator=new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);//为true代表以cglib动态代理方式生成代理类,默认false表示以JDK方式
return proxyCreator;
}
}
在自定义的shiro过滤器中,配置拦截方式的anno和authc,都是DefaultFilter枚举中的属性,还有很多其他属性,anno表示可以匿名访问不需要认证,authc表示需要认证。
web层控制器
@Controller
public class TestController {
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/idex")
public String index(){
return "index";
}
@RequestMapping("/loginUser")
public String loginUser(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession httpSession){
try{
//通常我们会将Subject对象理解为一个用户,同样的它也有可能是一个三方程序,它是一个抽象的概念,可以理解为任何与系统交互的“东西”都是Subject。
Subject subject= SecurityUtils.getSubject();
//将前端用户名和密码存入UsernamePasswordToken
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
//使用该登陆信息尝试登陆
subject.login(token);
//获取当前登陆的用户
User user=(User)subject.getPrincipal();
//将登陆用户存到session中
httpSession.setAttribute("user",user);
return "index";//登陆成功,跳转主页
}catch (Exception e){
//登陆失败,返回登录页
return "login";
}
}
}
页面配置与编写
在application.properties中加入以下配置
# jsp配置 前缀/后缀
spring.mvc.view.prefix=/pages/
spring.mvc.view.suffix=.jsp
在src下新建webapp/pages目录(默认在webapp目录下寻找,所以配置时从pages开始)
设置webapp为web模块
login页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Login</title>
</head>
<body>
<h2>欢迎登陆</h2>
<form action="/loginUser" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
<input type="submit" name="login"><br>
</form>
</body>
</html>
index页面
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<html>
<head>
<title>首页</title>
</head>
<body>
<h2>欢迎登陆,${user.username}</h2>
</body>
</html>
编写sql,配置
在UserMapper.xml中,修改为以下配置
<?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="com.sjh.shiro.mapper.UserMapper">
<resultMap id="userMap" type="com.sjh.shiro.model.User">
<id property="uid" column="uid"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<collection property="roleSet" ofType="com.sjh.shiro.model.Role">
<id property="rid" column="rid"/>
<result property="rname" column="rname"/>
<collection property="permissionSet" ofType="com.sjh.shiro.model.Permission">
<id property="pid" column="pid"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
</collection>
</collection>
</resultMap>
<select id="findByUsername" parameterType="string" resultMap="userMap">
SELECT u.*,r.*,p.*
FROM USER u
INNER JOIN user_role ur ON ur.`uid`=u.`uid`
INNER JOIN role r ON r.`rid`=ur.`rid`
INNER JOIN permission_role pr ON pr.`rid`=r.`rid`
INNER JOIN permission p ON p.`pid`=pr.`pid`
WHERE u.`username`= #{username};
</select>
</mapper>
测试
运行启动类,访问localhost:8080/index,由于需要权限,自动跳转至login
输入错误的用户名和密码,需要重新登陆,可以发现路径改变为处理登陆的loginUser
控制台输出
输入正确的用户名和密码
成功登陆
案例: 只要登陆即可访问所以页面
在shiro配置shiroFilter类的拦截方式里加入
filterChainDefinitionMap.put("/loginUser","anon");//登陆处理不需要认证
filterChainDefinitionMap.put("/**","user");//其他任何页面只要登陆即可访问
控制器加入方法
@RequestMapping("/admin")
@ResponseBody
public String admin(){
return "success";
}
@RequestMapping("/logout")
public String logout(){
//取出验证主体
Subject subject= SecurityUtils.getSubject();
//不为空则注销
if(subject!=null)
subject.logout();
return "login";
}
运行启动类,直接访问admin,会失败并跳到登陆页
成功登陆后即可访问
再访问logout测试一下注销,也是成功的
案例: 指定角色可访问页面
先在shiroFiter的拦截方式里加入
filterChainDefinitionMap.put("/admin","roles[admin]");//只允许admin的角色访问admin路径
表示只允许角色是admin的角色访问/admin路径。
因此在登陆时我们还需要把角色信息存入认证信息集中,在AuthRealm类中添加以下代码:
运行启动类,进行测试,之前创建数据库数据时,id为1用户名为kobe的用户角色是admin,id为2用户名为james的用户角色是customer
访问localhost:8080/admin,要求登陆,我们先输入角色不是admin的用户james
会发现没有权限访问,出现404错误
注销后使用角色为admin的用户登陆
此时是成功的
补充一个没有权限的页面,在控制器中增加方法
路径和之前配置拦截方式指定的路径名一样
@RequestMapping("/unAuthorized")
public String unAuthorized(){
return "unAuthorized";
}
在pages目录下新建一个unAuthorized.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h2>抱歉,您无权访问</h2>
</body>
</html>
此时再次用james访问admin,将会显示:
案例: 指定权限可访问页面
先在shiroFiter里配置权限拦截方式
filterChainDefinitionMap.put("/edit","perms[update]");//只允许update权限访问edit路径
在控制器类增加对应方法
@RequestMapping("/edit")
@ResponseBody
public String edit(){
return "success";
}
由于角色为admin的角色具有所有权限,而角色为customer的角色只有add权限和query权限,所以当我们用kobe访问edit时是成功的,用james则会失败。
配置拦截方式时
- 限制用户使用authc,anon,user等枚举
- 限制角色使用roles[角色名]
- 限制权限使用perms[权限名]
例:
Shiro优点:
- 提供了一套可用且简单的安全框架
- 灵活,应对需求能力强,web能力强
- 可于很多框架和应用进行集成
缺点:
- 学习资料少
- 除了RBAC外,操作的界面也需要自己实现