这次我们搭建一个springboot + mybatis + shiro + mysql + redis + thymeleaf + hutool + layui项目。期望能够实现这样的效果:
- 能对登录及其他请求做到控制
- 对后端接口能做权限控制
- 对前台页面能做权限控制
- 实现session共享
搭建springboot + shiro项目
数据库表准备
在这里我们准备3张表,分别为
- 用户表,表结构和数据如下:
- 角色表,表结构和数据如下:
- 权限表,表结构和数据如下:
实现登录
后台
创建一个springboot项目
可以参考文章《搭建spring boot + mybatis项目》,我们这里在之前搭建好的springboot项目的基础上进行。
自动生成代码
生成对应上面3张表的dao层,实体类等。编写对应的service层,dao层及*Mapper.xml层等。
能够实现需要的查询就好,这里不再赘述。
集成shiro
添加依赖
这里需要添加shiro依赖shiro-spring;
模板引擎我们用springboot推荐的thymeleaf,添加依赖spring-boot-starter-thymeleaf;
为了瘦身一些实体类(不写get/set方法),添加依赖lombok;
在登录时的验证码图片,我们用hutool提供的工具类完成,添加依赖hutool-all;
<!--Apache Shiro依赖包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!--引入thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.10</version>
</dependency>
添加shiro配置类
在config包下,新建一个ShiroConfig配置类,代码如下:
@Configuration
public class ShiroConfig {
@Resource
private ShiroUserService shiroUserService;
@Resource
private ShiroRoleMapper shiroRoleMapper;
// shiro的过滤器链
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/toLogin"); // 登录页面,请求被拦截后会重定向到登录页面
// shiroFilterFactoryBean.setSuccessUrl("/index"); // 这个是附件配置,一般情况不生效,原因看源码
// shiroFilterFactoryBean.setUnauthorizedUrl("/notRole"); // 跳转到没有权限的页面
// 过滤器链,从上到下,按顺序优先适配
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout"); // shiro内部已经实现了logout
filterChainDefinitionMap.put("/captcha_img", "anon");
//静态文件不拦截
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/layui/**", "anon");
//主要这行代码必须放在所有权限设置的最后,不然会导致所有 url 都被拦截 剩余的都需要认证
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// shiro的安全管理中心
@Bean
public SecurityManager securityManager(AuthorizingRealm customizeRealm) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(customizeRealm);
return defaultSecurityManager;
}
// 自定义域
@Bean
public AuthorizingRealm customizeRealm(){
AuthorizingRealm customizeRealm = new AuthorizingRealm() {
// 授权处理
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 认证处理
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = (String) token.getPrincipal();
String userPwd = new String((char[]) token.getCredentials());
//根据用户名从数据库查询用户
ShiroUser user = shiroUserService.findUserByUserName(userName);
if (user == null) {
throw new AccountException("用户名不正确");
} else if (!userPwd.equals(user.getPassword().toString())) {
throw new AccountException("密码不正确");
}
return new SimpleAuthenticationInfo(userName, user.getPassword().toString(), getName());
}
};
return customizeRealm;
}
}
登录接口
这里我们新建一个AccountController类,来处理登录相关操作。一共需要4个接口:登录页面页面接口;登录处理接口;展示首页接口。第四个是在登录时,利用hutool生成的一个验证码,可以不写,登录页面就没有验证码了。
@Controller
@Slf4j
public class AccountController {
// 登录页面
@RequestMapping(value = "/toLogin")
public String defaultLogin() {
return "login";
}
// 登录处理
@PostMapping(value = "/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password) throws UnknownAccountException, IncorrectCredentialsException, LockedAccountException {
// 从SecurityUtils里边templates创建一个 subject
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
return "redirect:index";
}
// 在认证提交前准备 token(令牌)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行认证登陆
try {
// 登录认证成功会自动重定向到"/index"
subject.login(token);
return "redirect:index";
} catch (ExcessiveAttemptsException eae) {
log.error("用户名或密码错误次数过多");
} catch (AuthenticationException ae) {
log.error("用户名或密码不正确");
}
return "login";
}
// 首页
@GetMapping("/index")
public String index(){
return "index";
}
// 利用hutool,生成验证码,登录时用
@GetMapping("/captcha_img")
public void create(HttpServletResponse response) throws IOException {
//定义图形验证码的长、宽、验证码字符数、干扰线宽度
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(116, 36, 4, 5);
//得到code
String code = captcha.getCode();
System.out.println(code);
//放到session
// session.setAttribute("code", code);
ServletOutputStream outputStream = response.getOutputStream();
captcha.write(outputStream);
outputStream.close();
}
前端
下面这些页面,可以直接从layui官方文档上复制下来稍改下来用,网址:https://www.layui.com/doc/element/layout.html
登录页面
在resource/template/下新创建一个login.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<base th:href="${#request.getContextPath()}+'/'">
<title>登录</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body>
<form class="layui-form" action="login" method="post">
<div class="layui-form-item">
<label class="layui-form-label">用户名</label>
<div class="layui-input-block">
<input type="text" name="username" value="test" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">密码框</label>
<div class="layui-input-inline">
<input type="password" name="password" value="123456" required lay-verify="required" placeholder="请输入密码" autocomplete="off" class="layui-input">
</div>
<div class="layui-form-mid layui-word-aux"><img src="captcha_img" onclick="changeImg(this)"></div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="formDemo">登录</button>
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
</div>
</div>
</form>
<script src="layui/layui.js"></script>
<script>
//一般直接写在一个js文件中
layui.use(['layer', 'form'], function(){
var layer = layui.layer
,form = layui.form;
layer.msg('Hello World');
});
</script>
<script>
// 点击验证码时,生成新的验证码
function changeImg(obj) {
obj.src = "captcha_img?count=1×tamp=" + new Date().getTime();
}
</script>
</body>
</html>
首页
在resource/template/下新创建一个index.html页面
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>index</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<div class="layui-header">
<div class="layui-logo">layui 后台布局</div>
<!-- 头部区域(可配合layui已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item"><a href="/role/list">角色</a></li>
<li class="layui-nav-item"><a href="">权限</a></li>
<li class="layui-nav-item"><a href="">用户</a></li>
<li class="layui-nav-item">
<a href="javascript:;">其它系统</a>
<dl class="layui-nav-child">
<dd><a href="">邮件管理</a></dd>
<dd><a href="">消息管理</a></dd>
<dd><a href="">授权管理</a></dd>
</dl>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;">
<img src="http://t.cn/RCzsdCq" class="layui-nav-img">
贤心
</a>
<dl class="layui-nav-child">
<dd><a href="">基本资料</a></dd>
<dd><a href="">安全设置</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="logout">退了</a></li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域(可配合layui已有的垂直导航) -->
<ul class="layui-nav layui-nav-tree" lay-filter="test">
<li class="layui-nav-item layui-nav-itemed">
<a class="" href="javascript:;">所有商品</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;">列表一</a></dd>
<dd><a href="javascript:;">列表二</a></dd>
<dd><a href="javascript:;">列表三</a></dd>
<dd><a href="">超链接</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">解决方案</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;">列表一</a></dd>
<dd><a href="javascript:;">列表二</a></dd>
<dd><a href="">超链接</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="">云市场</a></li>
<li class="layui-nav-item"><a href="">发布商品</a></li>
</ul>
</div>
</div>
<div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">内容主体区域</div>
</div>
<div class="layui-footer">
<!-- 底部固定区域 -->
© layui.com - 底部固定区域
</div>
</div>
<script src="layui/layui.js"></script>
<script>
//JavaScript代码区域
layui.use('element', function(){
var element = layui.element;
});
</script>
</body>
</html>
测试
启动项目,输入:http://localhost:8080/ ,展示页面如下:
接着点击登录按钮,显示页面如下:
对后台接口加权限控制
后台
修改shiro配置类
在ShiroConfig#customizeRealm()方法中,给当前登录对象授权,在重新自定义域的授权方法,代码如下:
// 授权处理
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<String> stringSet = new HashSet<>();
String username = (String) SecurityUtils.getSubject().getPrincipal();
ShiroUser user = shiroUserService.findUserByUserName(username);
// 把用户的角色都存到权限里(这里只是为了做演示)
List<ShiroRole> roleList = shiroRoleMapper.list(user.getId());
roleList.forEach(r -> stringSet.add("role:" + r.getRoleName()));
info.setStringPermissions(stringSet);
return info;
}
在ShiroConfig要给后台接口加权限控制,需要开启spring aop功能,代码如下:
// 实现spirng的自动代理
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
// 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
添加RoleController
添加一个RoleControler类,里面写个"/list"接口进行测试,如下:
@Controller
public class RoleController {
@GetMapping("/list")
@RequiresPermissions("role:admin") // 有role:admin权限的可以访问
// @RequiresPermissions("role:visitor") // 有role:visitor权限的可以访问
public String list(){
return "role/list";
}
}
前端
编写一个resource/template/role/list.html 页面,如果权限够,请可以访问到上面的“list”接口,展示到这个页面。代码如下:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<base th:href="${#request.getContextPath()}+'/'">
<title>角色列表</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body>
<table class="layui-table">
<colgroup>
<col width="150">
<col width="200">
<col>
</colgroup>
<thead>
<tr>
<th>昵称</th>
<th>加入时间</th>
<th>签名</th>
</tr>
</thead>
<tbody>
<tr>
<td>贤心</td>
<td>2016-11-29</td>
<td>人生就像是一场修行</td>
</tr>
<tr>
<td>许闲心</td>
<td>2016-11-28</td>
<td>于千万人之中遇见你所遇见的人,于千万年之中,时间的无涯的荒野里…</td>
</tr>
</tbody>
</table>
<script src="layui/layui.js"></script>
<script>
//一般直接写在一个js文件中
layui.use(['layer', 'form'], function(){
var layer = layui.layer
,form = layui.form;
layer.msg('Hello World');
});
</script>
</body>
</html>
测试
项目重启成功并且登录成功后,去访问 http://localhost:8080/list ,响应页面如下:
现在RoleController#list() 方法上的@RequiresPermissions(“role:admin”) 注解注释上,@RequiresPermissions(“role:visitor”) 注解的注释去掉,重启项目登录成功后,去访问
http://localhost:8080/list,页面如下:
看上面红色框中的报错信息,当前主体对象(当前登录用户)没有“role:visitor"权限。
对前端页面加权限控制
后台
pom中添加依赖
<!--thymeleaf和shiro整合包-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--thymeleaf转换HTML网页格式包 加完之后能够识别thymeleaf以外的扩展标签-->
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
application.yml中添加下面的配置
spring
thymeleaf:
mode: LEGACYHTML5 #支持网页格式为HTML5
shiro配置类ShiorConfig中添加下面配置
// 使得shiro标签能在前端页面使用
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
前端
我们以index.html测试,在该页面中引入下面的标签库
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
在index.html页面的body添加两个标签,如下:
整个index.html页面如下:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>index</title>
<link rel="stylesheet" href="layui/css/layui.css">
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
<div class="layui-header">
<div class="layui-logo">layui 后台布局</div>
<!-- 头部区域(可配合layui已有的水平导航) -->
<ul class="layui-nav layui-layout-left">
<li class="layui-nav-item"><a href="/role/list">角色</a></li>
<li class="layui-nav-item"><a href="">权限</a></li>
<li class="layui-nav-item"><a href="">用户</a></li>
<li class="layui-nav-item">
<a href="javascript:;">其它系统</a>
<dl class="layui-nav-child">
<dd><a href="">邮件管理</a></dd>
<dd><a href="">消息管理</a></dd>
<dd><a href="">授权管理</a></dd>
</dl>
</li>
</ul>
<ul class="layui-nav layui-layout-right">
<li class="layui-nav-item">
<a href="javascript:;">
<img src="http://t.cn/RCzsdCq" class="layui-nav-img">
贤心
</a>
<dl class="layui-nav-child">
<dd><a href="">基本资料</a></dd>
<dd><a href="">安全设置</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="logout">退了</a></li>
</ul>
</div>
<div class="layui-side layui-bg-black">
<div class="layui-side-scroll">
<!-- 左侧导航区域(可配合layui已有的垂直导航) -->
<ul class="layui-nav layui-nav-tree" lay-filter="test">
<li class="layui-nav-item layui-nav-itemed">
<a class="" href="javascript:;">所有商品</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;">列表一</a></dd>
<dd><a href="javascript:;">列表二</a></dd>
<dd><a href="javascript:;">列表三</a></dd>
<dd><a href="">超链接</a></dd>
</dl>
</li>
<li class="layui-nav-item">
<a href="javascript:;">解决方案</a>
<dl class="layui-nav-child">
<dd><a href="javascript:;">列表一</a></dd>
<dd><a href="javascript:;">列表二</a></dd>
<dd><a href="">超链接</a></dd>
</dl>
</li>
<li class="layui-nav-item"><a href="">云市场</a></li>
<li class="layui-nav-item"><a href="">发布商品</a></li>
</ul>
</div>
</div>
<div class="layui-body">
<!-- 内容主体区域 -->
<div style="padding: 15px;">内容主体区域</div>
<strong shiro:hasPermission="role:visitor">访客</strong>
<H1 shiro:hasPermission="role:admin">是管理员</H1>
</div>
<div class="layui-footer">
<!-- 底部固定区域 -->
© layui.com - 底部固定区域
</div>
</div>
<script src="layui/layui.js"></script>
<script>
//JavaScript代码区域
layui.use('element', function(){
var element = layui.element;
});
</script>
</body>
</html>
测试
重启项目,登录成功后进入首页,页面如下:
”是管理员“这个标签内容显示了,而访客标签没有显示,说明对前端页面的权限控制加成功了
shiro实现session共享
这里要借助redis实现session共享,首先启动本地的redis。
添加redis依赖
<!--引入redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml添加redis相关配置
# redis配置(可以不用配置,下面配置的值都是默认的)
redis:
host: localhost
port: 6379
expireTime: 60000 #这个不是默认的
这里需要重写sessionDao,并且用redis作为缓存
添加类ShrioRedisCache类,代码如下:
@Component
public class ShiroRedisCache<K, V> implements Cache<K, V> {
@Resource
private RedisTemplate<K,V> redisTemplate;
@Value("${spring.redis.expireTime}")
private long expireTime;
@Override
public V get(K k) throws CacheException {
return redisTemplate.opsForValue().get(k);
}
@Override
public V put(K k, V v) throws CacheException {
redisTemplate.opsForValue().set(k,v,expireTime, TimeUnit.SECONDS);
return null;
}
@Override
public V remove(K k) throws CacheException {
V v = redisTemplate.opsForValue().get(k);
redisTemplate.opsForValue().getOperations().delete(k);
return v;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set<K> keys() {
return null;
}
@Override
public Collection<V> values() {
return null;
}
}
添加ShrioRedisCacheManager类,代码如下:
@Component
public class ShiroRedisCacheManager implements CacheManager {
@Resource
private Cache shiroRedisCache;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return shiroRedisCache;
}
}
重新sessionDao,添加类,代码如下:
@Component
public class ShiroSessionDAO extends CachingSessionDAO {
@Override
protected void doUpdate(Session session) {
}
@Override
protected void doDelete(Session session) {
}
@Override
protected Serializable doCreate(Session session) {
// 这里绑定sessionId到session,必须要有
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
return null;
}
}
修改shiro配置类ShrioConfig,添加sessionManager的bean
@Bean
public DefaultSessionManager sessionManager(SessionDAO sessionDAO){
SimpleCookie simpleCookie = new SimpleCookie("shiro.sesssion"); // cookie名称
simpleCookie.setMaxAge(6000); // cookie过期时间
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(36000000); // session过期时间,默认30分钟
defaultWebSessionManager.setSessionIdCookie(simpleCookie);
defaultWebSessionManager.setSessionDAO(sessionDAO);
return defaultWebSessionManager;
}
再来把session管理器sessionManager和缓存管理器设置给shrio的安全管理器,ShiroConfig#securityManager()方法如下:
// shiro的安全管理中心
@Bean
public SecurityManager securityManager(AuthorizingRealm customizeRealm, CacheManager shiroRedisCacheManager, SessionManager sessionManager) {
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
defaultSecurityManager.setRealm(customizeRealm);
defaultSecurityManager.setCacheManager(shiroRedisCacheManager);
defaultSecurityManager.setSessionManager(sessionManager);
return defaultSecurityManager;
}
测试
启动该项目两个实例,端口分别为8080,8081。我们请求8080的服务进行登录,再去访问8081服务的首页,如下:
在去访问localhost:8081服务器的主页,就不用登录,可以直接访问到了