自建博客(day4之配置shiro)
上次做到了项目搭建到一办,后台实现结束了,前台半天的功夫的实现(其实做了好久,但基本上没遇上什么bug),这里主要想讲一下我的shiro配置,项目做完了,有很多自己不满意的地方,比如前台没有用户登录,没有用户注册,用户注册需要区分是管理员还是游客,游客没有访问后台的权限。
这里想做一个shiro用户管理,在这个过程中,我参考了springboot配置shiro教程
教程中的用的是mybatis,我的项目是dao曾使用的jpa,所以我要把mybatis的数据库操作,改造成jpa
项目需要的包导入
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro-web -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.3.2</version>
</dependency>
登录界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head th:replace="_fragments :: head(~{::title})">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册用户</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css" >
<link rel="stylesheet" href="../static/css/me.css" >
</head>
<body>
<nav th:replace="_fragments :: menu(1)" class="ui inverted attached segment m-padded-tb-mini m-shadow-small" ></nav>
<br>
<br>
<br>
<div class="m-container-small m-padded-tb-massive" style="max-width: 30em !important;">
<div class="ur container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">
用户登录
</div>
</h2>
<form class="ui large form" method="post" action="#" th:action="@{/login}">
<div class="ui segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" name="username" placeholder="用户名">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" name="password" placeholder="密码">
</div>
</div>
<button class="ui fluid large teal submit button">登 录</button>
</div>
<div class="ui error mini message"></div>
<div class="ui mini negative message" th:unless="${#strings.isEmpty(message)}" th:text="${message}">用户名和密码错误</div>
</form>
<div class="ui message">
新用户? <a href="/register">注册</a>
</div>
</div>
</div>
</div>
</div>
<footer th:replace="_fragments :: footer" class="ui inverted vertical segment m-padded-tb-massive">
</footer>
<!--/*/<th:block th:replace="_fragments :: script">/*/-->
<script src="https://cdn.jsdelivr.net/npm/aaa@qq.com/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<!--/*/</th:block>/*/-->
<script>
$('.ui.form').form({
fields : {
username : {
identifier: 'username',
rules: [{
type : 'empty',
prompt: '请输入用户名'
}]
},
password : {
identifier: 'password',
rules: [{
type : 'empty',
prompt: '请输入密码'
}]
}
}
});
</script>
</body>
</html>
注册界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head th:replace="_fragments :: head(~{::title})">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册用户</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css" >
<link rel="stylesheet" href="../static/css/me.css" >
</head>
<body>
<nav th:replace="_fragments :: menu(1)" class="ui inverted attached segment m-padded-tb-mini m-shadow-small" ></nav>
<br>
<br>
<br>
<div class="m-container-small m-padded-tb-massive"
style="max-width: 30em !important;">
<div class="ur container">
<div class="ui middle aligned center aligned grid">
<div class="column">
<h2 class="ui teal image header">
<div class="content">用户注册</div>
</h2>
<form class="ui large form" method="post" action="/register" th:object="${user}">
<div class="ui segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i> <input type="text" name="username"
placeholder="用户名">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i> <input type="password"
name="password" placeholder="密码">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i> <input type="text"
name="nickname" placeholder="昵称">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="envelope outline icon"></i> <input type="text"
name="email" placeholder="邮箱" >
</div>
</div>
<button class="ui fluid large teal submit button">注册</button>
</div>
<div class="ui error mini message"></div>
<div class="ui mini negative message" th:unless="${#strings.isEmpty(message)}" th:text="${message}">用户名和密码错误</div>
</form>
</div>
</div>
</div>
</div>
<br>
<footer th:replace="_fragments :: footer" class="ui inverted vertical segment m-padded-tb-massive">
</footer>
<!--/*/<th:block th:replace="_fragments :: script">/*/-->
<script src="https://cdn.jsdelivr.net/npm/aaa@qq.com/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<!--/*/</th:block>/*/-->
<script>
$('.ui.form').form({
fields : {
username : {
identifier: 'username',
rules: [{
type : 'empty',
prompt: '请输入用户名'
}]
},
password : {
identifier: 'password',
rules: [{
type : 'empty',
prompt: '请输入密码'
}]
}
}
});
</script>
</body>
</html>
权限不足界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head th:replace="_fragments :: head(~{::title})">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>错误</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.css" >
<link rel="stylesheet" href="../../static/css/me.css" >
</head>
<body>
<!--导航-->
<nav th:replace="_fragments :: menu(0)" class="ui inverted attached segment m-padded-tb-mini m-shadow-small" >
</nav>
<div class="m-container-small m-padded-tb-massive">
<div class="ui error message m-padded-tb-huge" >
<div class="ui contianer">
<h2>对不起</h2>
<p>权限不足,具体原因:</p>
<span th:text="${ex.message}"></span>
<a href="#" onClick="javascript:history.back()">返回</a>
</div>
</div>
</div>
<footer th:replace="_fragments :: footer" class="ui inverted vertical segment m-padded-tb-massive">
</footer>
</body>
</html>
表结构
RBAC 是当下权限系统的设计基础,同时有两种解释:
一: Role-Based Access Control,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角色
二:Resource-Based Access Control,基于资源的访问控制
即,你要能够删除产品,那么当前用户就必须拥有删除产品这样的权限
基于 RBAC 概念, 就会存在3 张基础表: 用户,角色,权限, 以及 2 张中间表来建立 用户与角色的多对多关系,角色与权限的多对多关系。 用户与权限之间也是多对多关系,但是是通过 角色间接建立的。
用户和角色是多对多,即表示:
一个用户可以有多种角色,一个角色也可以赋予多个用户。
一个角色可以包含多种权限,一种权限也可以赋予多个角色。
drop table if exists user;
drop table if exists role;
drop table if exists permission;
drop table if exists user_role;
drop table if exists role_permission;
create table user (
id bigint auto_increment,
name varchar(100),
password varchar(100),
constraint pk_users primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table role (
id bigint auto_increment,
name varchar(100),
constraint pk_roles primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table permission (
id bigint auto_increment,
name varchar(100),
constraint pk_permissions primary key(id)
) charset=utf8 ENGINE=InnoDB;
create table user_role (
uid bigint,
rid bigint,
constraint pk_users_roles primary key(uid, rid)
) charset=utf8 ENGINE=InnoDB;
create table role_permission (
rid bigint,
pid bigint,
constraint pk_roles_permissions primary key(rid, pid)
) charset=utf8 ENGINE=InnoDB;
Realm 概念
在 Shiro 中存在 Realm 这么个概念, Realm 这个单词翻译为 域,其实是非常难以理解的。
域 是什么鬼?和权限有什么毛关系? 这个单词Shiro的作者用的非常不好,让人很难理解。
那么 Realm 在 Shiro里到底扮演什么角色呢?
当应用程序向 Shiro 提供了 账号和密码之后, Shiro 就会问 Realm 这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限。
所以Realm 是什么? 其实就是个中介。 Realm 得到了 Shiro 给的用户和密码后,有可能去找 ini 文件,就像Shiro 入门中的 shiro.ini,也可以去找数据库,就如同本知识点中的 DAO 查询信息。
Realm 就是干这个用的,它才是真正进行用户认证和授权的关键地方
JPA Realm.就是用来通过数据库 验证用户,和相关授权的类。
两个方法分别做验证和授权:
doGetAuthenticationInfo(), doGetAuthorizationInfo()
细节在代码里都有详细注释,请仔细阅读。
注: DatabaseRealm 这个类,用户提供,但是不由用户自己调用,而是由 Shiro 去调用。 就像Servlet的doPost方法,是被Tomcat调用一样。
提供通过 JPA 进行验证的 Realm.
package com.wxl.realm;
import com.wxl.pojo.user.User;
import com.wxl.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 提供通过 JPA 进行验证的 Realm.
* 在 Shiro 中存在 Realm 这么个概念, Realm 这个单词翻译为 域,其实是非常难以理解的。
* 域 是什么鬼?和权限有什么毛关系? 这个单词Shiro的作者用的非常不好,让人很难理解。
* 那么 Realm 在 Shiro里到底扮演什么角色呢?
* 当应用程序向 Shiro 提供了 账号和密码之后, Shiro 就会问 Realm 这个账号密码是否对, 如果对的话,其所对应的用户拥有哪些角色,哪些权限。
* 所以Realm 是什么? 其实就是个中介。 Realm 得到了 Shiro 给的用户和密码后,去找数据库,就如同本知识点中的 DAO 查询信息。
*
* Realm 就是干这个用的,它才是真正进行用户认证和授权的关键地方。
*/
public class JPARealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
return s;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String userName = token.getPrincipal().toString();
User user = userService.getByUsername(userName);
String passwordInDB = user.getPassword();
String salt = user.getSalt();
//运算出来的密文(算法名称,密码,盐,加密次数)
//String encodedPassword = new SimpleHash(algorithmName,password,salt,times).toString();
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),
getName());
//返回加密后的密码
return authenticationInfo;
}
}
shiro 的配置文件
接着就是Shiro配置了。
按照Springboot的尿性,配置文件不要了, 用类来做。。。其实不是一样的么,还没有配置文件可读性强呢。。。
ShiroConfiguration 这个类就是shiro配置类,类名怎么命名都可以,就是要用@Configuration 注解表示它是一个配置类
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
这种写法就和 applicationContext-shiro.xml 中的
是同一个作用,只要用@Bean 注解了,就表示是被spring管理起来的对象了。
这个类里面就声明了SecurityManager,DatabaseRealm, HashedCredentialsMatcher,ShiroFilterFactoryBean 等等东西,只是写法变化了,其实和配置文件里是一样的。
需要注意一点,URLPathMatchingFilter 并没有用@Bean管理起来。 原因是Shiro的bug, 这个也是过滤器,ShiroFilterFactoryBean 也是过滤器,当他们都出现的时候,默认的什么anno,authc,logout过滤器就失效了。所以不能把他声明为@Bean。
public URLPathMatchingFilter getURLPathMatchingFilter() {
return new URLPathMatchingFilter();
}
package com.wxl.config;
import com.wxl.interceptor.URLPathMatchingFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.wxl.realm.JPARealm;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
@Bean
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
*
Filter Chain定义说明
1、一个URL可以配置多个Filter,使用逗号分隔
2、当设置多个过滤器时,全部验证通过,才视为通过
3、部分过滤器可指定参数,如perms,roles
*
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/admin/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/admin/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
//拦截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
//自定义拦截器
Map<String, Filter> customisedFilter = new HashMap<>();
customisedFilter.put("url", getURLPathMatchingFilter());
//配置映射关系
//权限配置
//anon表示此地址不需要任何权限即可访问
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/", "anon");
// filterChainDefinitionMap.put("/static/**", "anon");
//只对业务功能进行权限管理,权限配置本身不需要没有做权限要求,这样做是为了不让初学者混淆
filterChainDefinitionMap.put("/config/**", "anon");
filterChainDefinitionMap.put("/doLogout", "logout");
//所有的请求(除去配置的静态资源请求或请求地址为anon的请求)都要通过登录验证,如果未登录则跳到/login
filterChainDefinitionMap.put("/admin/*", "url");
shiroFilterFactoryBean.setFilters(customisedFilter);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
public URLPathMatchingFilter getURLPathMatchingFilter() {
return new URLPathMatchingFilter();
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getJPARealm());
return securityManager;
}
/**
* getJPARealm() 指定了 Realm 使用 JPARealm。
* @return
*/
@Bean
public JPARealm getJPARealm(){
JPARealm myShiroRealm = new JPARealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
/**
* 凭证匹配器
* hashedCredentialsMatcher() 指定了 加密算法使用 md5,并且混进行2次加密。
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
因为URLPathMatchingFilter 没有被声明为@Bean, 那么换句话说 URLPathMatchingFilter 就没有被Spring管理起来,那么也就无法在里面注入 PermissionService类了。
但是在业务上URLPathMatchingFilter 里面又必须使用PermissionService类,怎么办呢? 就借助SpringContextUtils 这个工具类,来获取PermissionService的实例。
这里提供工具类,下个步骤讲解如何使用这个工具类。
package com.wxl.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 因为URLPathMatchingFilter 没有被声明为@Bean, 那么换句话说 URLPathMatchingFilter 就没有被Spring管理起来,
* 那么也就无法在里面注入 PermissionService类了。
* 但是在业务上URLPathMatchingFilter 里面又必须使用PermissionService类,怎么办呢?
* 就借助SpringContextUtils 这个工具类,来获取PermissionService的实例。
* 这里提供工具类,下个步骤讲解如何使用这个工具类。
*/
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
public void setApplicationContext(ApplicationContext context)
throws BeansException {
SpringContextUtils.context = context;
}
public static ApplicationContext getContext(){
return context;
}
}
因为前面两个步骤的原因,既不能把 URLPathMatchingFilter.java 作为@Bean管理起来,又需要在里面使用 PermissionService,所以就用上一步的工具类 SpringContextUtils.java,通过如下方式获取 PermissionService了:
permissionService = SpringContextUtils.getContext().getBean(PermissionService.class);
package com.wxl.interceptor;
import com.wxl.service.PermissionService;
import com.wxl.util.SpringContextUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.PathMatchingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.util.Set;
public class URLPathMatchingFilter extends PathMatchingFilter {
@Autowired
PermissionService permissionService;
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue)
throws Exception {
if(null==permissionService)
permissionService = SpringContextUtils.getContext().getBean(PermissionService.class);
String requestURI = getPathWithinApplication(request);
System.out.println("requestURI:" + requestURI);
Subject subject = SecurityUtils.getSubject();
// 如果没有登录,就跳转到登录页面
// if (!subject.isAuthenticated()) {
// WebUtils.issueRedirect(request, response, "/admin");
// return false;
// }
// 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行)
System.out.println("permissionService:"+permissionService);
// 看看这个路径权限里有没有维护,如果没有维护,一律放行(也可以改为一律不放行)
boolean needInterceptor = permissionService.needInterceptor(requestURI);
//如果数据库不存在该权限,表示没有对该路径访问的维护,则放行,返回ture
if (!needInterceptor) {
return true;
} else {
boolean hasPermission = false;
String userName = subject.getPrincipal().toString();
//获取某个用户所拥有的权限地址集合
Set<String> permissionUrls = permissionService.listPermissionURLs(userName);
System.out.println(userName+"拥有的权限"+permissionUrls);
for (String url : permissionUrls) {
// 这就表示当前用户有这个权限
if (url.equals(requestURI)) {
hasPermission = true;
break;
}
}
//如果该用户存在该路径的权限,则放行通过
if (hasPermission)
return true;
else {
UnauthorizedException ex = new UnauthorizedException("当前用户没有访问路径 " + requestURI + " 的权限");
System.out.println(ex.getMessage());
subject.getSession().setAttribute("ex", ex);
WebUtils.issueRedirect(request, response, "/unauthorized");
return false;
}
}
}
}
关于service与dao就不细说了,下面我会把项目上传到资源里。
运行结果:
主页面
用户登录界面
用户注册页面
登录成功后前台效果
游客登录后台
游客权限不足
推荐阅读
-
自建博客(day4之配置shiro)
-
一头坑进Redis之常见redis.conf配置介绍 博客分类: redis HBaseRedis
-
我使用过的Linux命令之ifconfig - 网络配置命令 博客分类: Linux命令 LinuxVmwareifconfigeth0network
-
Hexo个人博客之yilia主题的安装和配置
-
springmvc环境搭建之配置和所需jar包 博客分类: java javaspringspringmvc
-
[一起学Hive]之四-Hive的安装配置 博客分类: hive hivehive安装配置
-
权限之-shiro 博客分类: shiro shiro
-
Linux Maven 安装与配置 博客分类: 服务器操作系统之Linux linuxmaven
-
Linux搭建Zookeeper环境之单机模式和集群模式配置 博客分类: 软件架构技术栈 linuxzk配置管理集群模式单机模式
-
shiro之INI配置详解