Shiro入门:
ApacheShiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
Shiro有三大核心组件:
Subject:即当前用户,在权限管理的应用程序里往往需要知道谁能够操作什么,谁拥有操作该程序的权利,shiro中则需要通过Subject来提供基础的当前用户信息,Subject 不仅仅代表某个用户,与当前应用交互的任何东西都是Subject,如网络爬虫等。所有的Subject都要绑定到SecurityManager上,与Subject的交互实际上是被转换为与SecurityManager的交互。
SecurityManager:即所有Subject的管理者,这是Shiro框架的核心组件,可以把他看做是一个Shiro框架的全局管理组件,用于调度各种Shiro框架的服务。作用类似于SpringMVC中的DispatcherServlet,用于拦截所有请求并进行处理。
Realm:Realm是用户的信息认证器和用户的权限人证器,我们需要自己来实现Realm来自定义的管理我们自己系统内部的权限规则。SecurityManager要验证用户,需要从Realm中获取用户。可以把Realm看做是数据源。
本文重点在于在springboot项目中集成Shiro,不在于讲Shiro原理,话不多说,上干货:
工程结构如下:
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<modules>
<module>web</module>
<module>dao</module>
<module>service</module>
<module>common</module>
</modules>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<!--<version>2.1.2.RELEASE</version>-->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.loan</groupId>
<artifactId>supermarket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>supermarket</name>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- springboot监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 热部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>runtime</scope>
</dependency>
<!-- 日志工具类 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- SpringBoot集成thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--<dependency>-->
<!--<groupId>org.springframework.boot</groupId>-->
<!--<artifactId>spring-boot-starter-data-jpa</artifactId>-->
<!--</dependency>-->
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.1.2</version>
</dependency>
<!--常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<!--集合常用工具类 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.2</version>
</dependency>
<!-- json处理-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.54</version>
</dependency>
<!--验证码 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.5.1</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>cobertura</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这是我的项目中用到的所有依赖项,但是继承shiro,只需要用到:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
数据库sql如下:
-- 权限表 --
CREATE TABLE permission (
pid int(11) NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL DEFAULT '',
url VARCHAR(255) DEFAULT '',
PRIMARY KEY (pid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO permission VALUES ('1', 'add', '');
INSERT INTO permission VALUES ('2', 'delete', '');
INSERT INTO permission VALUES ('3', 'edit', '');
INSERT INTO permission VALUES ('4', 'query', '');
-- 用户表 --
CREATE TABLE user(
uid int(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL DEFAULT '',
password VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (uid)
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
INSERT INTO user VALUES ('1', 'admin', '123');
INSERT INTO user VALUES ('2', 'demo', '123');
-- 角色表 --
CREATE TABLE role(
rid int(11) NOT NULL AUTO_INCREMENT,
rname VARCHAR(255) NOT NULL DEFAULT '',
PRIMARY KEY (rid)
) 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);
建好表如下图:
实体类如下:
User存在:1、唯一ID 2、用户名 3、密码 4、拥有的Role
public class User {
private Integer uid;
private String username;
private String password;
private Set<Role> roles = new HashSet<>();
}
Role存在:1、唯一ID 2、角色名 3、角色拥有的权限 4、拥有该角色的用户(可以不需要)
public class Role {
private Integer rid;
private String rname;
private Set<Permission> permissions = new HashSet<>();
private Set<User> users = new HashSet<>();
}
Permission存在:1、唯一ID 2、权限名 3、。。。
public class Permission {
private Integer pid;
private String name;
private String url;
}
dao层如下:
@Repository
public interface UserDao {
User findUserByUsername(@Param("username") String username);
}
service代码如下:
@Service
public class UserServiece {
@Autowired
private UserDao userDao;
public List<User> getUser() {
return userDao.queryUserList();
}
public List<User> queryUserByName(String name) {
return userDao.queryUserByName(name);
}
public User findByUsername(String username) {
return userDao.findUserByUsername(username);
}
}
mapper.xml的文件中添加对应sql
<?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.loan.supermarket.dao.UserDao">
<!-- Result Map-->
<resultMap id="BaseResultMap" type="com.loan.supermarket.mapper.User">
<result column="password" property="password"/>
<result column="uid" property="uid"/>
<result column="username" property="username"/>
<collection property="roles" ofType="com.loan.supermarket.mapper.Role">
<id property="rid" column="rid"/>
<result property="rname" column="rname"/>
<collection property="permissions" ofType="com.loan.supermarket.mapper.Permission">
<id property="pid" column="pid"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
</collection>
</collection>
</resultMap>
<select id="findUserByUsername" parameterType="com.loan.supermarket.mapper.User" resultMap="BaseResultMap">
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 pr.pid = p.pid
WHERE u.username = #{username}
</select>
</mapper>
添加Shiro的配置文件:
import com.loan.supermarket.web.shiro.AuthRealm;
import com.loan.supermarket.web.utils.CredentialMatcher;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.LinkedHashMap;
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
// 登陆界面
bean.setLoginUrl("/login");
// 成功登陆后的界面
bean.setSuccessUrl("/index");
// 没有权限访问的界面
bean.setUnauthorizedUrl("/unauthorized");
// 定制相关表单是否需要相关权限的设定,具体配置信息请看:Shiro-内置的FilterChain
HashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// index界面需要鉴权
filterChainDefinitionMap.put("/index", "authc");
// login、loginUser表单不需要验证
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/loginUser", "anon");
filterChainDefinitionMap.put("/unauthorized", "anon");
// admin表单需要角色 admin 才能访问
filterChainDefinitionMap.put("/admin", "roles[admin]");
// edit表单需要权限 edit 才能访问
filterChainDefinitionMap.put("/edit", "perms[edit]");
filterChainDefinitionMap.put("/druid/**", "anon");
filterChainDefinitionMap.put("/**", "user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
@Bean("authRealm")
public AuthRealm authRealm() {
AuthRealm authRealm = new AuthRealm();
/**
* shiro自带的MemoryConstrainedCacheManager作缓存只能用于本机,那么在集群时就无法使用,
* 如果使用ehcache、redis等其他缓存,可以参考https://www.cnblogs.com/lic309/p/4072848.html
*/
authRealm.setCacheManager(new MemoryConstrainedCacheManager());
// 用com.mmall.demo2.CredentialMatcher中自定义的密码比较器对密码进行比较
authRealm.setCredentialsMatcher(new CredentialMatcher());
return authRealm;
}
// 把shiro和spring进行绑定
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// 开启自动代码,设置为true即可
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
编写控制类代码:
import com.loan.supermarket.mapper.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpSession;
@Controller
public class TestController {
private static final Logger log =
LoggerFactory.getLogger(TestController.class);
@RequestMapping("login")
public String login() {
return "login";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/logout")
public String logout() {
// 先验证主体
Subject subject = SecurityUtils.getSubject();
if (subject != null) {
subject.logout();
}
return "login";
}
@RequestMapping("unauthorized")
public String unauthorized() {
return "unauthorized";
}
@RequestMapping("/admin")
@ResponseBody
public String admin() {
return "admin success";
}
@RequestMapping("/edit")
@ResponseBody
public String edit() {
return "edit success";
}
@RequestMapping("/loginUser")
public String loginUser(@RequestParam("username") String username,
@RequestParam("password") String password,
HttpSession session) {
// 初始化这个用户的token
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取事件的主体
Subject subject = SecurityUtils.getSubject();
try {
// 尝试登录
subject.login(token);
// 获取用户的全部信息
User user = (User) subject.getPrincipal();
// 用于界面输出
session.setAttribute("user", user);
log.info("{} is login !!", username);
return "index";
} catch (Exception e) {
log.error("{} has something wrong !!", username);
return "unauthorized";
}
}
}
Realm是shiro框架中需要自己开发逻辑的内容,作用是对用户和用户权限进行验证:
import com.loan.supermarket.mapper.Permission;
import com.loan.supermarket.mapper.Role;
import com.loan.supermarket.mapper.User;
import com.loan.supermarket.service.UserServiece;
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.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public class AuthRealm extends AuthorizingRealm {
@Autowired
private UserServiece userServiece;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取User用户
User user = (User) principals.fromRealm(this.getClass().getName()).iterator().next();
List<String> permissionList = new ArrayList<>();
List<String> roleNameList = new ArrayList<>();
Set<Role> roleSet = user.getRoles();
if (roleSet != null) {
for (Role role : roleSet) {
roleNameList.add(role.getRname());
Set<Permission> permissionSet = role.getPermissions();
if (permissionSet != null) {
for (Permission permission : permissionSet) {
permissionList.add(permission.getName());
}
}
}
}
// 需要把角色和权限放入info中
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 权限设定
info.addStringPermissions(permissionList);
// 角色设定
info.addRoles(roleNameList);
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String username = usernamePasswordToken.getUsername();
User user = userServiece.findByUsername(username);
if (null != user) {
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
} else {
return new SimpleAuthenticationInfo();
}
}
}
配置文件中,对于Realm配置的时候会用到自己对于用户验证的匹配器:
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
//@Component
public class CredentialMatcher extends SimpleCredentialsMatcher
{
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info)
{
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String password = new String(usernamePasswordToken.getPassword());
String dbPassword = (String) info.getCredentials();
if(StringUtils.isNotBlank(password)&&StringUtils.isNotBlank(dbPassword)){
return this.equals(password, dbPassword);
}
return false;
}
}
由于这次我们把shiro基础配置直接放在代码中,所以application.properties文件可以沿用之前的文件内容,无需修改:
# 端口配置
server.port=8000
## 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
# 初始化时建立物理连接的个数
spring.datasource.druid.initial-size=5
# 最大连接池数量
spring.datasource.druid.max-active=30
# 最小连接池数量
spring.datasource.druid.min-idle=5
# 获取连接时最大等待时间,单位毫秒
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 连接保持空闲而不被驱逐的最小时间
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 用来检测连接是否有效的sql,要求是一个查询语句
spring.datasource.druid.validation-query=SELECT 1 FROM DUAL
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
spring.datasource.druid.test-while-idle=true
# 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-borrow=false
# 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
spring.datasource.druid.test-on-return=false
# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
spring.datasource.druid.pool-prepared-statements=true
# 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=50
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计
spring.datasource.druid.filters=stat,wall
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat=true
# druid连接池监控
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=123
# 排除一些静态资源,以提高效率
spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
# mybatis
mybatis.type-aliases-package=com.loan.supermarket.mapper
mybatis.mapper-locations=classpath:mapping/*.xml
mybatis.configuration.map-underscore-to-camel-case=true
# THYMELEAF (ThymeleafAutoConfiguration)
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
# ;charset=<encoding> is added
#spring.thymeleaf.content-type=text/html
# set to false for hot refresh
spring.thymeleaf.cache=false
spring.devtools.restart.enabled=true
另外我们使用thymeleaf写几个用户登录相关的页面,用来验证功能:
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<h1>欢迎登录 : <span th:text="${session.user.username}"></span></h1>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>登录</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<h1>欢迎登录</h1>
<form action="/loginUser" method="post">
<input type="text" name="username"><br/>
<input type="password" name="password"><br/>
<input type="submit" value="提交"><br/>
</form>
</body>
</html>
unauthorized.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Unauthorized</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
</head>
<body>
<h1>Unauthorized!</h1>
</body>
</html>
代码传送门:https://github.com/bruceq/supermarket
撸完代码,我们开始展示Shiro功能的实现效果:
服务启动后,访问登录界面,输入admin用户的用户名和密码:
验证成功后,可直接跳转到index页面:
如果我们填写错误的用户名和密码,会跳转到unauthorized页面:
至此,代码和相关展示全部结束,另附学习Shiro时相关链接:
《Shiro简介》
《在前后端分离的SpringBoot项目中集成Shiro权限框架》