1

学习了一下Shrio,简单的做一下笔记
推荐三篇文章,有需要可以看看:

30分钟学会如何使用Shiro

SpringBoot2.0集成Shiro

SpringBoot整合Shiro,通过用户、角色、权限三者关联实现权限管理

Shiro 关于

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。相比较 Spring Security,Shiro 要小巧、简单的多。

一、准备

本文使用的 SpringBoot + MyBatis + thymeleaf

先看看项目结构

clipboard.png

clipboard.png

1.pom.xml

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.16</version>
        </dependency>

只贴出来关键依赖,其他可自行Maven

2.数据库 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 idx_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','3');
INSERT INTO permission_role VALUES ('1','4');

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');


-- 为了验证加密效果 后面会被我用 ShiroMD5Util.MD5Pwd(),生成密码在 手动更改user表的密码  --

application.yml
DruidConfiguration文件就不写出来了

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
    username: user
    password: 123

    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
  #thymelea模板配置
  thymeleaf:
    cache: false
#    mybatis
mybatis:
  mapper-locations: mappers/*.xml
  type-aliases-package: com.scitc.shiro.model

二、基础代码(Model,Dao和Service层)

1.Model 实体

User.java

package com.scitc.shiro.model;

import java.util.HashSet;
import java.util.Set;

public class User {
    private Integer uid;
    private String username;
    private String  password;
    private Set<Role> roles = new HashSet<>();

    public Integer getUid() {
        return uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Set<Role> getRoles() {
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

    @Override
    public String toString() {
        return "User{" +
                "uid=" + uid +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", roles=" + roles +
                '}';
    }
}

Role.java

package com.scitc.shiro.model;

import java.util.HashSet;
import java.util.Set;

public class Role {
    private Integer rid;
    private String rname;
    private Set<Permission>  permissions = new HashSet<>();
    private Set<User> users =  new HashSet<>();

    public Integer getRid() {
        return rid;
    }

    public void setRid(Integer rid) {
        this.rid = rid;
    }

    public String getRname() {
        return rname;
    }

    public void setRname(String rname) {
        this.rname = rname;
    }

    public Set<Permission> getPermissions() {
        return permissions;
    }

    public void setPermissions(Set<Permission> permissions) {
        this.permissions = permissions;
    }

    public Set<User> getUsers() {
        return users;
    }

    public void setUsers(Set<User> users) {
        this.users = users;
    }

    @Override
    public String toString() {
        return "Role{" +
                "rid=" + rid +
                ", rname='" + rname + '\'' +
                ", permissions=" + permissions +
                ", users=" + users +
                '}';
    }
}

Permission.java

package com.scitc.shiro.model;

public class Permission {

    private Integer pid;
    private  String name;
    private String url;

    public Integer getPid() {
        return pid;
    }

    public void setPid(Integer pid) {
        this.pid = pid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @Override
    public String toString() {
        return "Permission{" +
                "pid=" + pid +
                ", name='" + name + '\'' +
                ", url='" + url + '\'' +
                '}';
    }
}

2.mapper

UserMapper.java

public interface UserMapper {
   User findByUsername(@Param("username") String username);
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.scitc.shiro.mapper.UserMapper">
    
        <resultMap id="userMap" type="com.scitc.shiro.model.User">
            <id column="uid" property="uid"/>
            <result column="username" property="username"/>
            <result column="password" property="password"/>
            <collection property="roles" ofType="com.scitc.shiro.model.Role">
                <id column="rid" property="rid"/>
                <result column="rname" property="rname"/>
                <collection property="permissions" ofType="com.scitc.shiro.model.Permission">
                    <id column="pid" property="pid"/>
                    <result column="name" property="name"/>
                    <result column="url" property="url"/>
                </collection>
            </collection>
        </resultMap>
    
        <select id="findByUsername" parameterType="String" resultMap="userMap">
          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 pr.pid = p.pid
          WHERE u.username = #{username}
        </select>
    </mapper>
    
    //用户表--->用户角色表--->角色表--->角色权限表--->权限表

UserService

UserService.java

public interface UserService {
    User  findByUsername(String username);
}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    @Override
    public User findByUsername(String username) {
       return userMapper.findByUsername(username);
    }
}

三、整合Shiro

先定义一个 加密工具类 ShiroMD5Util.java
用户名一般是唯一的,所以盐值为:username + "salt"
若用户名后面可以修改,为保证两次(注册,登录)加密结果一致,可将 盐值添加到数据表的字段中

package com.scitc.shiro.utils;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class ShiroMD5Util {
    public static String MD5Pwd(String username, String pwd) {
        //可用户注册、登录 时使用
        // 加密算法MD5
        // salt盐 username + salt
        // 迭代次数 加密2次
        String md5Pwd = new SimpleHash("MD5", pwd, ByteSource.Util.bytes(username + "salt"), 2).toHex();
        return md5Pwd;
    }
}

ShiroConfiguration.java

package com.scitc.shiro.config;

import com.scitc.shiro.CredentialMatcher;
import com.scitc.shiro.realm.AuthRealm;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
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.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;

@Configuration
public class ShiroConfiguration {
    //4.
    @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");//访问 没有权限的页面时,将会被重定向到此链接,需在 Controller设置页面
//        1). anon 可以被 匿名访问
//        2). authc 必须认证(登录)才能访问
//        3). logout 登出
//        4). roles 角色过滤器
        LinkedHashMap<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/index","authc");
        filterChainDefinitionMap.put("/login","anon");
        filterChainDefinitionMap.put("/loginuser","anon");
        filterChainDefinitionMap.put("/admin","roles[admin]");
        filterChainDefinitionMap.put("/edit","perms[edit]");//具有edit权限的才能访问
        filterChainDefinitionMap.put("/druid/**","anon");//开放druid的监控后台

        filterChainDefinitionMap.put("/**","user");
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }

    //3.
    //  import org.apache.shiro.mgt.SecurityManager; 这个包
    @Bean("securityManager")
    public SecurityManager securityManager(@Qualifier("authRealm") AuthRealm authRealm){
        DefaultWebSecurityManager manager  = new DefaultWebSecurityManager();
        manager.setRealm(authRealm);
        return manager;
    }

    //2
    @Bean("authRealm")
    public AuthRealm authRealm(@Qualifier("credentialMatcher") CredentialMatcher matcher){
        AuthRealm authRealm = new AuthRealm();
        // 相关认证缓存到cache中
        authRealm.setCacheManager(new MemoryConstrainedCacheManager());
        authRealm.setCredentialsMatcher(matcher);
        return authRealm;
    }


   // 1 密码采用加密方式进行验证:
    // 本文使用的是自定义的校验规则,
    // 另一种方式是 在bean 中 配置相关的加密算法
    @Bean("credentialMatcher")
    public CredentialMatcher credentialMatcher(){
        return new CredentialMatcher();
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
    //Shiro与Spring的关联 开启利用注解配置权限
    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     *
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager")SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

AuthRealm.java

自定义的AuthRealm继承AuthorizingRealm。
并且重写父类中的doGetAuthorizationInfo(权限相关)、
doGetAuthenticationInfo(身份认证)这两个方法。

package com.scitc.shiro.realm;

import com.scitc.shiro.model.Permission;
import com.scitc.shiro.model.Role;
import com.scitc.shiro.model.User;
import com.scitc.shiro.service.UserService;
import org.apache.commons.collections.CollectionUtils;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class AuthRealm extends AuthorizingRealm {
    private static final Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
    @Autowired
    private UserService userService;
    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        /**
         *  User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
         *  User getAfterUser  = (User)principalCollection.getPrimaryPrincipal();
         *  两者效果一致,都是获取 user对象,
         *  两者打印结果(登录用户为 admin):
         *   User{uid=1, username='admin', password='4b91fc877a3f4df7e812f98ebde4b5e5', roles=[Role{rid=1, rname='admin', permissions=[Permission{pid=1, name='add', url=''},
         *   Permission{pid=4, name='query', url=''}, Permission{pid=2, name='delete', url=''}, Permission{pid=3, name='edit', url=''}],
         */
        //类似 session中获取用户
        User user = (User) principalCollection.fromRealm(this.getClass().getName()).iterator().next();
        List<String> permissionList = new ArrayList<>();
        List<String> roleNameList = new ArrayList<>();
        //获取用户的角色,再获取角色拥有的所有权限
        // 登录时数据库已经把所有的东西都获取出来了
        Set<Role> roleSet = user.getRoles();
        if (CollectionUtils.isNotEmpty(roleSet)) {
            for (Role role : roleSet) {
                roleNameList.add(role.getRname());
                Set<Permission> permissionSet = role.getPermissions();
                if (CollectionUtils.isNotEmpty(permissionSet)) {
                    for (Permission permission : permissionSet) {
                        permissionList.add(permission.getName());
                    }
                }
            }
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //所有操作权限
        info.addStringPermissions(permissionList);//[delete, add, edit, query]
        //授权时拿到了角色
        info.addRoles(roleNameList);//[admin]

        return info;
    }

    //认证登录 (执行完将会执行 CredentialMatcher.doCredentialsMatch()方法)
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        User user = userService.findByUsername(username);
        return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
    }
}

CredentialMatcher.java

package com.scitc.shiro;
import com.scitc.shiro.utils.ShiroMD5Util;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;

public class CredentialMatcher extends SimpleCredentialsMatcher {

    //密码校验规则的重写
    // AuthRealm 中 doGetAuthenticationInfo 执行之后会到达这里来 进行密码比对
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
        /**
         *  获取到用户输入的密码,进行盐值加密,加密方式与注册时相同,再与数据库中已加过密的密码进行比较
         */
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String password = new String(usernamePasswordToken.getPassword());
        //把用户输入的密码 进行加密
        password = ShiroMD5Util.MD5Pwd(((UsernamePasswordToken) token).getUsername(),password);
        //数据库中的密码
        String dbPassword = (String) info.getCredentials();
        return this.equals(password, dbPassword);
    }
}

TestController

四、验证登录

TestController.java

package com.scitc.shiro.controller;

import com.scitc.shiro.model.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpSession;

@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";
    }

    //注解 测试
    //用户必须拥有 delete 权限才能访问
    @RequestMapping("/delete")
    @RequiresPermissions("delete")
    @ResponseBody
    public String delete() {
        return "我是test delete方法,测试注解是否有效";
    }

    @RequestMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            subject.logout();
        }
        return "login";
    }
    @PostMapping("/loginuser")
    public String loginUser(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            HttpSession session) {


        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            User user = (User) subject.getPrincipal();
            session.setAttribute("user", user);
            return "redirect:index";
        } catch (Exception e) {
            return "redirect:login";
        }
    }

}

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<h1>登录 </h1>
<form th:action="@{/loginuser}" method="post">
    username:<input type="text" name="username"><br/>
    password:<input type="password" name="password"><br/>
    <input type="submit" value="提交">
</form>
</body>
</html>

index.html

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>

<h1 >欢迎登录:   [[${session.user.username}]]</h1>

<a th:href="@{/admin}">Admin Page</a><br/><br/>

<a th:href="@{/edit}">Edit Page</a><br/><br/>

<a th:href="@{/logout}">Logout</a>
</body>
</html>

unauthorized.html(没有权限就到此页面来 )

<!DOCTYPE html>
<html lang="en"xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>unauthorized</title>
</head>
<body>

<h1 >unauthorized</h1>

</body>
</html>

admin用户登录后访问,因为拥有所有权限, 所有页面不限制

clipboard.png

访问 Admin Page

clipboard.png

访问 Edit Page

clipboard.png

访问 Delete Page

clipboard.png

退出后: demo用户登录访问

clipboard.png

访问 Admin Page

clipboard.png

访问 Edit Page

clipboard.png

访问 Delete Page

clipboard.png


小咸鱼
11 声望4 粉丝

一只不断学习的小咸鱼