博客系统项目源码
上次做到了项目搭建到一办,后台实现结束了,前台半天的功夫的实现(其实做了好久,但基本上没遇上什么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/jquery@3.2/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/jquery@3.2/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,基于角色的访问控制
即,你要能够删除产品,那么当前用户就必须拥有产品经理这个角***r> 二: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就不细说了,下面我会把项目上传到资源里。

运行结果:

主页面

用户登录界面

用户注册页面

登录成功后前台效果

游客登录后台

游客权限不足