欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

自建博客(day4之配置shiro)

程序员文章站 2024-03-25 13:30:46
...

上次做到了项目搭建到一办,后台实现结束了,前台半天的功夫的实现(其实做了好久,但基本上没遇上什么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)
用户登录界面
自建博客(day4之配置shiro)
用户注册页面
自建博客(day4之配置shiro)
登录成功后前台效果
自建博客(day4之配置shiro)
游客登录后台
自建博客(day4之配置shiro)
游客权限不足
自建博客(day4之配置shiro)

相关标签: 自建博客