最近因公司业务需要,研究了一下shiro框架,虽然在研究过程中也遇到过不少问题,不过好在最后都解决了。于是在这里写一篇文章记录一下自己的使用过程。本篇文章只讨论shiro的快速使用,暂不考虑原理分析,后续可能会出源码分析的文章。
话不多说,我们马上进入正题,本篇文章采用springboot2.0,jdk1.8。
先贴一张项目结构
一、引入相关依赖并添加配置
首先我们在搭建好的springboot框架中引入maven依赖。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
当然除此之外还需要引入web相关依赖,如mysql,druid,mybatis等,同时我们在测试的过程中使用的时thymeleaf,相关的maven请自行添加。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
添加完成后我们在application中添加相关的配置,主要是druid,thymeleaf的相关配置。
server:
port: 8099
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
platform: mysql
#注意修改成自己的仓库名称哦
url: jdbc:mysql://localhost:3306/candy?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
# 连接池设置
druid:
initial-size: 5
min-idle: 5
max-active: 20
# 配置获取连接等待超时的时间
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 300000
# Oracle请使用select 1 from dual
validation-query: SELECT 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall,log4j2
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
use-global-data-source-stat: true
#thymeleaf模板引擎
thymeleaf:
mode: HTML5
encoding: utf-8
servlet:
content-type: text/html
#开发时关闭缓存
cache: false
suffix: .html
prefix: classpath:/templates/ #这里表示页面存放的位置是resource目录下的templates文件夹。
mybatis:
mapper-locations: classpath:mapper/*.xml #这里表示mapper的路径是resources文件下mapper文件夹下所有.xml结尾的文件,请修改成你自己的mapper存放的地址
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql、参数、查询结果
type-aliases-package: com.candy.shiro.entity #这里修改成你自己的实体类的路径
该配置文件中需要修改的地方基本上已经标注出来了,请注意修改,不要直接粘过去使用哦。
二、创建数据库表
本次项目一共需要5张表
其中比较重要的是用户表,角色表,权限表。具体得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 id_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','4');
insert INTO permission_role VALUES('1','3');
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);
三、生成项目结构,编写sql
这里我直接使用easy code直接生成了项目结构,非常好用,推荐大家使用。当然你也可以使用其它得代码自动生成工具,如果你得项目结构已经生成了,请继续向下看。
我们找到刚才生成的User实体类,在里面加上
private Set<Role> roles = new HashSet<>();
并生成getter setter方法;同时在Role实体类中添加
private Set<Permission> permissions = new HashSet<>();
private Set<User> users = new HashSet<>();
并生成getter setter 方法
添加完成之后我们在UserDao中添加一个方法
/**
* @Author : qzx
* @Description : //TODO 通过名字查询用户
* @Date : 16:34 2019/6/13
* @Param : [username]
* @return : com.candy.shiro.entity.User
**/
User findByUserName(@Param("username") String username);
通过用户名查找对应的用户,添加完成后再UserMapper.xml中编写对应的sql
新建一个 resultMap
<resultMap id="userMap2" type="com.candy.shiro.entity.User">
<id property="uid" column="uid" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<collection property="roles" ofType="com.candy.shiro.entity.Role">
<id property="rid" column="rid"/>
<result property="rname" column="rname"/>
<collection property="permissions" ofType="com.candy.shiro.entity.Permission">
<id column="pid" property="pid"/>
<result property="name" column="name"/>
<result property="url" column="url"/>
</collection>
</collection>
</resultMap>
使我们在查询User的同时也会查到该User的角色和该角色拥有的权限。
接下来编写sql语句:
<select id="findByUserName" parameterType="string" resultMap="userMap2">
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>
连接查询出用户的角色和权限。
写完了这些别忘了在service层中调用这个方法哦。
这样我们就做完了前期的准备工作了,接下来开始进入重点!!!
四、编写shiro配置文件
简单的实现shiro做登录验证,我们只需要编写两个配置类,AuthRealm配置类和ShiroConfiguration配置类,其中AuthRealm的主要目的使从前端接收我们的登录用户的参数,并将参数发送给shiro框架的SimpleAuthorizationInfo类进行验证。ShiroConfiguration配置类主要是为了定义角色或权限可以访问的路径,以及一些基本的配置。
首先我们编写AuthRealm类
package com.candy.shiro.conf.shiro;
import com.candy.shiro.entity.Permission;
import com.candy.shiro.entity.Role;
import com.candy.shiro.entity.User;
import com.candy.shiro.service.UserService;
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.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.ArrayList;
import java.util.Set;
/**
* @author :q'z'x
* @ClassName :AuthRealm
* @date : 2019/6/13 16:22
* @description : TODO
*/
public class AuthRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
/**
* @Author : qzx
* @Description : //TODO 从session里面获取用户,并根据该用户查询所在的角色和该角色的权限
* @Date : 16:54 2019/6/13
* @Param : [principalCollection]
* @return : org.apache.shiro.authz.AuthorizationInfo
**/
User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
ArrayList<String> permissionList = new ArrayList<>();
ArrayList<String> roleNameList = new ArrayList<>();
Set<Role> roleSet = user.getRoles();
//遍历该用户的角色和权限,并存放到list中
if(!CollectionUtils.isEmpty(roleSet)){
for (Role role : roleSet){
roleNameList.add(role.getRname());
Set<Permission> permissionSet = role.getPermissions();
if(!CollectionUtils.isEmpty(permissionSet)){
for(Permission permission : permissionSet){
permissionList.add(permission.getName());
}
}
}
}
//将角色和权限交给shiro处理
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissionList);
info.addRoles(roleNameList);
return info;
}
//认证登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
//这个接口改成自己的接口
User user = userService.findByUserName(username);
return new SimpleAuthenticationInfo(user,user.getPassword(),this.getClass().getName());
}
}
这个配置类里有登录验证和授权验证。登录认证主要是把该用户的所有信息都查询到,主要还是看授权认证。授权认证需要把用户从session中获取,然后将用户的角色和该角色的权限全部遍历出来,交给shrio处理。
另一个配置类代码如下:
package com.candy.shiro.conf.shiro;
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.apache.shiro.web.servlet.SimpleCookie;
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.LinkedHashMap;
/**
* @author :qzx
* @ClassName :ShiroConfiguration
* @date : 2019/6/13 17:32
* @description : TODO
*/
@Configuration
public class ShiroConfiguration {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager manager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(manager);
//登录
bean.setLoginUrl("/login");
//登录成功之后跳转
bean.setSuccessUrl("/index");
//没有请求访问的时候
bean.setUnauthorizedUrl("/unauthorized");
//以下是某些请求如何拦截
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//DefaultFilter类里有关于authc,anon的相关信息,这是shiro默认的几个验证方法
filterChainDefinitionMap.put("/index","authc");
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/loginUser","anon");
//只允许角色为admin的用户访问
filterChainDefinitionMap.put("/admin","roles[admin]");
//表示具有edit的权限才可以访问
filterChainDefinitionMap.put("/edit","perms[edit]");
//这个必须放在最后面表示其它接口是不需要登录认证的
filterChainDefinitionMap.put("/**","user");
bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return bean;
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(authRealm);
return manager;
}
@Bean("authRealm")
public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher){
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(matcher);
//开启缓存
authRealm.setCacheManager(new MemoryConstrainedCacheManager());
return authRealm;
}
@Bean("credentialMatcher")
public CredentialMatcher credentialMatcher(){
return new CredentialMatcher();
}
/**
* rememberMe cookie 效果是重开浏览器后无需重新登录
*
* @return SimpleCookie
*/
private SimpleCookie rememberMeCookie() {
// 设置 cookie 名称,对应 login.html 页面的 <input type="checkbox" name="rememberMe"/>
SimpleCookie cookie = new SimpleCookie("rememberMe");
// cookie.setSecure(true); // 只在 https中有效 注释掉 正常
// 设置 cookie 的过期时间,单位为秒,这里为一天
cookie.setMaxAge(2000);
return cookie;
}
//shiro和Spring相关联
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager manager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(manager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
}
shiroFilterFactoryBean是里面最重要的一个类了,每一步的意思我都已经加注释了,比较关键的就setFilterChainDefinitionMap里面的配置了,比如说filterChainDefinitionMap.put("/index",“authc”); 这句代码表示想要访问index这个路径的页面,则必须经过表单验证通过才可以。filterChainDefinitionMap.put("/admin",“roles[admin]”);表示只有角色admin才可以访问/admin这个路径。具体的可以进到DefaultFilter类里查看实现。
另外要注意的是filterChainDefinitionMap.put("/**",“user”);这行代码必须放在最后,不然会出问题的。
到这里为止我们的准备工作就差不多了,下面我们要写controller类了。
五、编写controller类和页面并进行测试
package com.candy.shiro.controller;
import com.candy.shiro.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
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 org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* @author :qzx
* @ClassName :TestController
* @date : 2019/6/13 18:07
* @description : TODO
*/
@Controller
public class TestController {
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@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("/logout")
public String logout() {
//取出当前验证的主体
Subject subject = SecurityUtils.getSubject();
if(subject != null){
//退出登录
subject.logout();
}
return "login";
}
@RequestMapping("/loginUser")
public String loginUser(@RequestParam("username") String username, @RequestParam("password") String password, HttpSession session) {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
Subject subject = SecurityUtils.getSubject();
//shiro的认证逻辑
try {
subject.login(token);
User user = (User) subject.getPrincipal();
session.setAttribute("user", user);
return "index/index";
} catch (Exception e) {
return "login";
}
}
}
controller类的代码很好理解,基本上不用解释,注意自己的路径,有可能和我的不一样。
接下来我们在templates下面写页面,页面结构参考我最开始发的那张结构图(如果想和我的路径一样的话)。
login的页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</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="提交" />
</form>
</body>
</html>
index的页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h1>欢迎,您已登录成功!</h1>
</body>
</html>
到这里我们就可以启动一下进行测试了,启动项目,打开浏览器输入 http://localhost:8099/login 进入到登录页面(如果没有进行登录的话,访问其它接口会自动跳转到登录页面)。
输入账号密码(admin;123)会进入登录成功界面:
这是我们修改地址后缀为edit,点击访问,会访问到以下页面,说明访问成功了,admin拥有访问edit接口的权限。
接下来我们将后缀改为logout退出登录,重新输入账号密码(demo;123)点击访问登录成功后,将后缀改为edit,会出现如下假面
系统自动跳转到unauthorized接口,说明该用户没有权限访问edit(可以自己定义一个无权限访问的页面)。
六、结语
到这里为止,我们的shiro使用方法就都已经介绍完毕了,不知道小伙伴有没有学会呢?如果学会了使用方法,建议自己打断点走一走内部实现流程,通过源码分析来进一步加深对shiro框架的理解和操作。喜欢本篇文章或者有什么想问的欢迎在评论区留言。