1. 权限管理

1.1 什么是权限管理

基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问并且只能访问被授权的资源。

1.2 什么是身份认证

身份认证 就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。

1.3 什么是身份授权

身份授权 即访问控制,控制谁能访问那些资源,不同的用户应该拥有不同的资源访问权限,常见的有学校的教务管理系统:有教师,学生,管理员登录几个模块,以不同的身份登录就会显示不同的界面!

2. 什么是shiro

shiro是Apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,是一个功能强大且简单易用的Java安全框架,它可以用来进行身份验证,授权,加密和会话管理。

3. shiro核心架构

3.1 Subject

Subject即主体,外部应用与Subject进行交互,Subject对象记录了当前操作的用户,将用户的概念理解成当前操作的主体,可能是一个浏览器的请求的用户,也可能是一个允许的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过Subject对象进行认证和授权,而Subject对象是通过SecurityManager进行认证和授权!

3.2 SecurityManager

SecurityManager即安全管理器,对全部的Subject对象进行安全管理,它是shiro的核心,通过SecurityManager可以完成对Subject对象的认证授权等系列操作,实质上是使用Authenticator进行认证,使用Authorizer进行授权,使用SessionManager进行会话管理!

3.3 Authenticator

Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供了ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足认证的大部分需求,也可以使用自定义的认证器。

3.4 Authorizer

Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否由此功能的操作权限。

3.5 Realm

Realm即领域,相当于datasource,SecurityManager进行安全认证是需要从Realm获取用户权限数据,比如:用户的身份信息如果放到数据库中,那么就需要从数据库中获取到用户身份信息。

注:不要把Realm认为只是在里面取数据,在Realm还有认证和授权相关的代码!

3.6 SessionManager

SessionManager即会话管理,shiro框架定义了一系列会话管理,它不依赖于web的session,所以shiro可以使用在非web的环境下,也可以将分布式的应用集中在一起管理,此特性可以实现单点登录!

3.7 SessionDao

SessionDao即会话dao,是对session会话操作的一套接口,比如将session存储到数据库,也可以使用jdbc将session存入数据库。

3.8 CacheManager

CacheManager即缓存管理器,将用户权限数据存储到缓存中,可以提高性能。

3.9 Cryptography

Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发,比如提供了常用分散列,盐值计算,加/解密等功能!

4. shiro的认证

4.1 认证

认证就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。

4.2 认证的关键对象

  • Subject:主体

访问系统的用户,主体可以是程序,用户等,进行认证的都称为主体。

  • Principal:身份信息

是主体进行身份认证的标识,标识必须具有唯一性,如电话号码,手机号,邮箱地址,一个主体可以有多个身份,但必须有一个主身份!

  • credential:凭证信息

只有主体自己知道的安全信息,如密码,证书等。

4.3 认证流程

4.4 认证的开发

使用本地shiro.ini文件开发

  1. 创建maven项目引入依赖
<!--引入shiro相关依赖-->
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.5.3</version>
    </dependency>
  1. 引入shiro.ini配置文件
  2. 开发认证代码
/** * 测试认证(加载本地的realm) * */
public class TestAuthenticator
{
   
    public static void main( String[] args )
    {
   
        //1.创建SecurityManager对象
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));  //可以从数据库中获取realm
        //2.将安装工具类设置为默认的安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //3.获取主体对象
        Subject subject = SecurityUtils.getSubject();
        //4.创建token令牌
        UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
        //5.执行登录方法
        try {
   
            subject.login(token);
            System.out.println("登陆成功!!");
        }catch (UnknownAccountException e){
   
            e.printStackTrace();
            System.out.println("用户名错误!!");
        }catch (IncorrectCredentialsException e){
   
            e.printStackTrace();
            System.out.println("密码错误!!");
        }
    }
}

4.5 自定义Realm的开发

使用本地shiro.ini开发,使用的realm就是我们本地提供的,而实际开发中,realm的数据往往是我们的数据库中读取!

  1. shiro提供的Realm

  2. Realm的实现类中认证是使用SimpleAccountRealm

    SimpleAccountRealm 部分源码中有两个方法,分别是认证:doGetAuthenticationInfo(AuthenticationToken token),授权:doGetAuthorizationInfo(PrincipalCollection principals)

//判断用户是否为合法用户
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
   
        UsernamePasswordToken upToken = (UsernamePasswordToken)token;
        SimpleAccount account = this.getUser(upToken.getUsername());
        if (account != null) {
   
            if (account.isLocked()) {
   
                throw new LockedAccountException("Account [" + account + "] is locked.");
            }

            if (account.isCredentialsExpired()) {
   
                String msg = "The credentials for account [" + account + "] are expired";
                throw new ExpiredCredentialsException(msg);
            }
        }

        return account;
    }

//为合法用户授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
   
        String username = this.getUsername(principals);
        this.USERS_LOCK.readLock().lock();

        AuthorizationInfo var3;
        try {
   
            var3 = (AuthorizationInfo)this.users.get(username);
        } finally {
   
            this.USERS_LOCK.readLock().unlock();
        }

        return var3;
    }
  1. 自定义Realm
/** * 自定义realm,分别实现认证和授权方法 */
public class CustomerRealm extends AuthorizingRealm {
   
    //身份认证
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
   
        return null;
    }
    //授权
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   
        //获取用户名key
        String principal = (String) authenticationToken.getPrincipal();
        //根据前台传递过来的principal身份信息去使用jdbc或者mybatis查询数据库
        if("liuzeyu".equals(principal)){
   
            /** * principal:返回当前数据库中的正确用户名 * credentials:返回当前数据库中正确的密码 * realmName:当前realm名字 */
            return new SimpleAuthenticationInfo(principal,"809080",this.getName());
        }
        return null;
    }
}
  1. 使用自定义Realm进行认证
/** * 测试加载远程数据库的realm */
public class TestCustomerAuthenticator {
   

    public static void main(String[] args) {
   
        //1.创建SecurityManager
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(new CustomerRealm());
        //2.将安装工具设置为默认的安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //3.获取主体对象
        Subject subject = SecurityUtils.getSubject();
        //4.获取令牌
        UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu", "809080");

        //5.执行
        try {
   
            subject.login(token);
            System.out.println("登陆成功!!");
        }catch (UnknownAccountException e){
   
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
   
            e.printStackTrace();
            System.out.println("密码错误");
        }
    }
}

4.6 对密码进行加盐和hash

  1. 测试加密
/** * 测试hash,salt,散列 */
public class TestHash {
   
    public static void main(String[] args) {
   
        //1.不加salt和散列次数
        Md5Hash md5Hash1 = new Md5Hash("809080");
        System.out.println(md5Hash1.toHex()); //转成16进制字符串
        //2.加salt,不添加散列次数
        Md5Hash md5Hash2 = new Md5Hash("809080","Xq*07");
        System.out.println(md5Hash2.toHex());
        //3.加salt,添加散列次数1024次
        Md5Hash md5Hash3 = new Md5Hash("809080","Xq*07",1024);
        System.out.println(md5Hash3.toHex());
    }
}
  1. 为明文加入加密算法
public class CustomerReleamMd5 extends AuthorizingRealm {
   
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
   
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   
        String principal = (String) authenticationToken.getPrincipal();
        //利用假数据去匹配
        if("liuzeyu".equals(principal)){
   
            return new SimpleAuthenticationInfo(principal,
                    "641acb1f58ed7e24677a1e26d2497604",  //传递不同的密码强度验证
                    ByteSource.Util.bytes("Xq*07"),
                    this.getName());
        }
        return null;
    }
}

public class TestCustomerMd5Authenticator {
   
    public static void main(String[] args) {
   

        //身份验证
        //1.1创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //设置自定义realm获取认证数据
        CustomerReleamMd5 releamMd5 = new CustomerReleamMd5();

        //创建凭证匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");  //(md5)算法
        hashedCredentialsMatcher.setHashIterations(1024);  //散列1024次,默认是一次
        releamMd5.setCredentialsMatcher(hashedCredentialsMatcher); //为凭证设置凭证适配器(hash)

        defaultSecurityManager.setRealm(releamMd5);
        //1.2设置安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //1.3获取主体
        Subject subject = SecurityUtils.getSubject();
        //1.4获取令牌
        UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
        //1.5执行
        try {
   
            subject.login(token);
            System.out.println("登陆成功");
        }catch (UnknownAccountException e){
   
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
   
            e.printStackTrace();
            System.out.println("密码错误");
        }

    }
}

5. shiro的授权

在自定义的Realm基础上加入授权功能,只有通过认证用户才可以被授权!

  1. 自定义Realm
public class CustomerReleamMd5 extends AuthorizingRealm {
   
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
   
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        System.out.println(primaryPrincipal);
        System.out.println("primaryPrincipal:"+primaryPrincipal);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole("admin");  //添加角色admin
        simpleAuthorizationInfo.addStringPermission("product:*:01"); //权限字符串 product 可以对01实例执行任何操作
        return simpleAuthorizationInfo;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   
        String principal = (String) authenticationToken.getPrincipal();
        //利用假数据去匹配
        if("liuzeyu".equals(principal)){
   
            return new SimpleAuthenticationInfo(principal,
                    "641acb1f58ed7e24677a1e26d2497604",  //传递不同的密码强度验证
                    ByteSource.Util.bytes("Xq*07"),
                    this.getName());
        }
        return null;
    }
}

  1. 测试
public class TestCustomerMd5Authenticator {
   
    public static void main(String[] args) {
   

        //身份验证
        //1.1创建安全管理器
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        //设置自定义realm获取认证数据
        CustomerReleamMd5 releamMd5 = new CustomerReleamMd5();

        //创建凭证匹配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");  //(md5)算法
        hashedCredentialsMatcher.setHashIterations(1024);  //散列1024次,默认是一次
        releamMd5.setCredentialsMatcher(hashedCredentialsMatcher); //为凭证设置凭证适配器(hash)

        defaultSecurityManager.setRealm(releamMd5);
        //1.2设置安全管理器
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        //1.3获取主体
        Subject subject = SecurityUtils.getSubject();
        //1.4获取令牌
        UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080");
        //1.5执行
        try {
   
            subject.login(token);
            System.out.println("登陆成功");
        }catch (UnknownAccountException e){
   
            e.printStackTrace();
            System.out.println("用户名错误");
        }catch (IncorrectCredentialsException e){
   
            e.printStackTrace();
            System.out.println("密码错误");
        }

        //2.授权
        //如果认证通过
        if(subject.isAuthenticated()){
   
            //基于角色管理
            boolean admin = subject.hasRole("admin");
            System.out.println(admin);
            //基于资源管理
            boolean permitted = subject.isPermitted("product:create:01");
            System.out.println(permitted);
        }
    }
}

6. spring boot整合shiro

模板引擎使用原生的JSP,配合ShiroFilter实现用户的认证和授权,实现用户注册,登录,可以以不同的身份显示不同的资源!

1. spring boot前期准备

  1. 导入依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

<!-- springboot启动shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.5.3</version>
        </dependency>

<!-- shiro默认缓存-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.5.3</version>
        </dependency>

<!--redis整合springboot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

<!-- 引入springboot解析jsp依赖-->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>jstl</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

<!-- 引入mybatis相关的启动依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.2</version>
        </dependency>
<!-- mysql相关依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.38</version>
        </dependency>

<!-- druid数据源-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.19</version>
        </dependency>
    </dependencies>
  1. 准备SQL文件
/* Navicat Premium Data Transfer Source Server : xiaochen的数据库 Source Server Type : MySQL Source Server Version : 50718 Source Host : 127.0.0.1:3306 Source Schema : shiro Target Server Type : MySQL Target Server Version : 50718 File Encoding : 65001 Date: 29/05/2020 16:31:22 */

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_perms`;
CREATE TABLE `t_perms` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(80) DEFAULT NULL,
  `url` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_perms
-- ----------------------------
BEGIN;
INSERT INTO `t_perms` VALUES (1, 'user:*:*', '');
INSERT INTO `t_perms` VALUES (2, 'product:*:01', NULL);
INSERT INTO `t_perms` VALUES (3, 'order:*:*', NULL);
COMMIT;

-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `name` varchar(60) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_role
-- ----------------------------
BEGIN;
INSERT INTO `t_role` VALUES (1, 'admin');
INSERT INTO `t_role` VALUES (2, 'user');
INSERT INTO `t_role` VALUES (3, 'product');
COMMIT;

-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
  `id` int(6) NOT NULL,
  `roleid` int(6) DEFAULT NULL,
  `permsid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_role_perms
-- ----------------------------
BEGIN;
INSERT INTO `t_role_perms` VALUES (1, 1, 1);
INSERT INTO `t_role_perms` VALUES (2, 1, 2);
INSERT INTO `t_role_perms` VALUES (3, 2, 1);
INSERT INTO `t_role_perms` VALUES (4, 3, 2);
INSERT INTO `t_role_perms` VALUES (5, 1, 3);
COMMIT;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` int(6) NOT NULL AUTO_INCREMENT,
  `username` varchar(40) DEFAULT NULL,
  `password` varchar(40) DEFAULT NULL,
  `salt` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_user
-- ----------------------------
BEGIN;
INSERT INTO `t_user` VALUES (1, 'xiaochen', '24dce55acdcb3b6363c7eacd24e98cb7', '28qr0xu%');
INSERT INTO `t_user` VALUES (2, 'zhangsan', 'ca9f1c951ce2bfb5669f3723780487ff', 'IWd1)#or');
COMMIT;

-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
  `id` int(6) NOT NULL,
  `userid` int(6) DEFAULT NULL,
  `roleid` int(6) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_user_role
-- ----------------------------
BEGIN;
INSERT INTO `t_user_role` VALUES (1, 1, 1);
INSERT INTO `t_user_role` VALUES (2, 2, 2);
INSERT INTO `t_user_role` VALUES (3, 2, 3);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

  1. application.properties配置文件
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

spring.application.name=shiro
server.servlet.context-path=/shiro
server.port=8888

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.username=root
spring.datasource.password=809080

spring.datasource.url=jdbc:mysql://localhost:3306/shiro?useUnicode=true&useSSL=false&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver


mybatis.type-aliases-package=com.liuzeyu.entity
mybatis.mapper-locations=classpath:com/liuzeyu/mapper/*.xml


logging.level.com.liuzeyu.mapper=debug

#相关配置
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
  1. mapper层开发
@Mapper
public interface UserMapper {
   

    public void save(User user);

    public User findByUsername(String username);


    public User findRolesByUsername(String username);

    public List<Perms> findPermsByRoleId(Integer id);
}


表和表之间的关系:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liuzeyu.mapper.UserMapper">
    <insert id="save" useGeneratedKeys="true" parameterType="user" keyProperty="id" >
        insert into t_user values(#{id},#{username},#{password},#{salt})
    </insert>

    <select id="findByUsername" parameterType="String" resultType="user">
        select * from t_user where username=#{username}
    </select>

<!--查询的是一个用户对应的多个角色信息-->
    <resultMap id="userMap" type="User">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <collection property="roles" javaType="list" ofType="Role">
            <id column="rid" property="id"/>
            <result column="name" property="name"/>
        </collection>
    </resultMap>

    <select id="findRolesByUsername" parameterType="String" resultMap="userMap">
        SELECT
            u.id,u.username,r.id rid,r.name
        from
            t_user u
        left join
            t_user_role ur
        on
            u.id = ur.userid
        left join
            t_role r
        on
            r.id = ur.roleid
        where
            u.username = #{username}
    </select>


    <select id="findPermsByRoleId" parameterType="Integer" resultType="Perms">
        select p.id,p.name,r.name
        from
            t_role r
        left join
            t_role_perms rp
        on
            rp.roleid=r.id
        left join
            t_perms p
        on
            p.id = rp.permsid
        where r.id=#{id}
    </select>
</mapper>
  1. service层开发
public interface UserService {
   

    public void save(User user);

    public User findByUsername(String username);

    public User findRolesByUsername(String username);

    public List<Perms> findPermsByRoleId(Integer id);

}
@Service
@Transactional
public class UserServiceImpl implements UserService {
   

    @Autowired
    private UserMapper mapper;
    @Override
    public void save(User user) {
   
        //获取盐
        String salt = SaltUtils.getSalt(8);
        user.setSalt(salt);
        Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024);
        user.setPassword(md5Hash.toHex());
        mapper.save(user);
    }

    @Override
    public User findByUsername(String username) {
   
        return mapper.findByUsername(username);
    }

    @Override
    public User findRolesByUsername(String username) {
   
        return mapper.findRolesByUsername(username);
    }

    @Override
    public List<Perms> findPermsByRoleId(Integer id) {
   
        return mapper.findPermsByRoleId(id);
    }
}

2. shrio框架的整合!

  1. ShiroConfig配置
@Configuration
public class ShiroConfig {
   

    //1.创建ShiroFilter
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
   
        //创建Shiro的filter
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //注入安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);

        //配置系统受限资源
        Map<String,String> map = new HashMap<String,String>();
        map.put("/user/login", "anon");   //将不需要授权的资源放最上面
        map.put("/user/register", "anon");
        map.put("/register.jsp","anon");
        map.put("/**", "authc");  //authc请求这个资源需要认证和授权。/**表示对所有的资源认证
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        //shiroFilterFactoryBean.setLoginUrl("/login.jsp");  默认请求受限资源跳转login.jsp
        //配置系统公共资源


        return shiroFilterFactoryBean;
    }

    //2.创建web安全管理器
    @Bean
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("realm") Realm realm){
   
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm);

        return defaultWebSecurityManager;
    }

    @Bean("realm")
    //创建自定义realm
    public Realm getRealm(){
   
        CustomerRealm customerRealm = new CustomerRealm();
        //配置凭证适配器
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        hashedCredentialsMatcher.setHashIterations(1024);

        customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);


        //开启shiro默认的ehcache缓存机制
        customerRealm.setCachingEnabled(true);   //启动缓存
        customerRealm.setAuthorizationCachingEnabled(true);  //启动认证缓存
        customerRealm.setAuthorizationCacheName("authorizationCache"); //命名
        customerRealm.setAuthenticationCachingEnabled(true);   //启动授权缓存
        customerRealm.setAuthenticationCacheName("authenticatonCache"); //命名
        customerRealm.setCacheManager(new RedisCacheManager()); //使用Redis提供的缓存机制
        return customerRealm;
    }
}
  1. Realm开发
public class CustomerRealm extends AuthorizingRealm {
   

    @Autowired
    private UserService userService;

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
   
        String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
        //假数据
// if("liuzeyu12a".equals(primaryPrincipal)){
   
// SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// simpleAuthorizationInfo.addRole("user");
// simpleAuthorizationInfo.addStringPermission("user:create:*");
// simpleAuthorizationInfo.addStringPermission("user:save:*");
// }
        User user = userService.findRolesByUsername(primaryPrincipal);
        if(!ObjectUtils.isEmpty(user.getRoles())){
   
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            user.getRoles().forEach(role->{
   
                simpleAuthorizationInfo.addRole(role.getName());
                List<Perms> perms = userService.findPermsByRoleId(role.getId());
                if(!CollectionUtils.isEmpty(perms)){
   
                    perms.forEach(perm->{
   
                        simpleAuthorizationInfo.addStringPermission(perm.getName());
                    });
                }
            });
            return simpleAuthorizationInfo;
        }
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
   
        String principal = (String) authenticationToken.getPrincipal();
        //连接数据库做查询用户名是否存在
//假数据 if("liuzeyu12a".equals(principal)){
   
// return new SimpleAuthenticationInfo(principal, "809080", this.getName());
// }

        User user = userService.findByUsername(principal);
        if(!ObjectUtils.isEmpty(user)){
   
            return new SimpleAuthenticationInfo(principal,
                    user.getPassword(),
                    new MyByteSource(user.getSalt()),
                    this.getName());
        }
        return null;
    }
}

  1. Redis缓存实现了Shiro提供的CacheManager
public class RedisCacheManager implements CacheManager {
   
// 参数:认证或者授权的名字
    @Override
    public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
   
        System.out.println("缓存名称:"+cacheName);
        return new RedisCache<K,V>(cacheName);
    }
}
  1. Reids缓冲器
public class RedisCache<K,V> implements Cache<K,V> {
   

    private String cacheName;

    public RedisCache(String cacheName) {
   
        this.cacheName = cacheName;
    }

    public RedisCache() {
   
    }

    @Override
    public V get(K k) throws CacheException {
   
        System.out.println("获取缓存:"+k);
        return (V) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
    }

    @Override
    public V put(K k, V v) throws CacheException {
   
        System.out.println("设置缓存:"+"k:"+k+" value:"+v);
        getRedisTemplate().opsForHash().put(this.cacheName,k.toString(),v);
        return null;
    }

    @Override
    public V remove(K k) throws CacheException {
   
        return (V) getRedisTemplate().opsForHash().delete(this.cacheName,k.toString());
    }

    @Override
    public void clear() throws CacheException {
   
        getRedisTemplate().delete(this.cacheName);
    }

    @Override
    public int size() {
   
        return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
    }

    @Override
    public Set<K> keys() {
   
        return getRedisTemplate().opsForHash().keys(this.cacheName);
    }

    @Override
    public Collection<V> values() {
   
        return getRedisTemplate().opsForHash().values(this.cacheName);
    }

    //封装获取redisTempalte
    private RedisTemplate getRedisTemplate(){
   
        RedisTemplate redisTemplate = (RedisTemplate)ApplicationContextUtils.getBean("redisTemplate");
        //设置序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

3. 工具类和实体类

  1. ApplicationContextUtils:
/** * 功能就是从IOC容器中获取bean */
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
   

    private static ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
   
        this.applicationContext = applicationContext;
    }

    //根据bean名称从工厂中获取bean对象
    public static Object getBean(String name){
   
        return  applicationContext.getBean(name);
    }
}
  1. 自定义salt实现,实现序列化接口
//自定义salt实现,实现序列化接口
public class MyByteSource extends SimpleByteSource implements Serializable {
   

    public MyByteSource(String string) {
   
        super(string);
    }
}
  1. 获取随机盐
/** * 获取随机盐 */
public class SaltUtils {
   

    public static String getSalt(int n){
   
        StringBuilder sb = new StringBuilder();

        String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
        "abcdefghijklmnopqrstuvwxyz!@#$%^&*()";
        char[] code = chars.toCharArray();
        for (int i = 0; i < n; i++) {
   
            sb.append(code[new Random().nextInt(code.length)]);
        }
        return sb.toString();
    }
}
  1. 实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class User  implements Serializable {
   
    private Integer id;
    private String username;
    private String password;
    private String salt;

    private List<Role> roles;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Role implements Serializable {
   

    private Integer id;
    private String name;

    private List<Perms> perms;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
public class Perms implements Serializable {
   
    private Integer id;
    private String name;
    private String url;
}

4. web层开发

  1. controller层开发
@Controller
@RequestMapping("/user")
public class UserController {
   

    @Autowired
    private UserService service;

    /** * 处理身份验证 * @param username * @param password * @return */
    @PostMapping("/login")
    public String login(String username,String password){
   
        Subject subject = SecurityUtils.getSubject();
        try {
   
            subject.login(new UsernamePasswordToken(username,password));
            System.out.println("登陆成功");
            return "redirect:/index.jsp";
        }catch (UnknownAccountException e){
   
            e.printStackTrace();
            System.out.println("用户名不存在");
        }catch (IncorrectCredentialsException e){
   
            e.printStackTrace();
            System.out.println("密码错误");
        }
        return "redirect:/login.jsp";
    }

    /** * 用户退出登录 * @return */
    @GetMapping("logout")
    public String logout(){
   
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "redirect:/login.jsp";
    }

    @PostMapping("/register")
    public String register(User user){
   
        try {
   
            service.save(user);
            System.out.println("注册成功");
            return "redirect:/login.jsp";
        }catch (Exception e){
   
            e.printStackTrace();
            System.out.println("注册失败");
            return "redirect:/register.jsp";
        }

    }
}

  1. JSP界面

login.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>登录页面</h2>
<form action="${pageContext.request.contextPath}/user/login" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit" value="登录"><br>
</form>
</body>
</html>

register.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>注册页面</h2>
<form action="${pageContext.request.contextPath}/user/register" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit" value="立即注册"><br>
</form>
</body>
</html>

index.jsp

<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<h1>V0.1 系统主页</h1>
<ul>
    <shiro:hasAnyRoles name="admin,user">
        <li><a href="">用户管理</a>
            <ul>
                <ui>
                    <shiro:hasPermission name="user:create:*">
                        <li>创建</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:delete:01">
                        <li>删处</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:update:01">
                        <li>修改</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:select:01">
                        <li>查询</li>
                    </shiro:hasPermission>
                </ui>
            </ul>
        </li>
    </shiro:hasAnyRoles>

    <shiro:hasAnyRoles name="admin,product,order">
        <shiro:hasPermission name="product:*:01">
            <li><a href="">商品管理</a></li>
        </shiro:hasPermission>
        <shiro:hasPermission name="order:*:01">
            <li><a href="">订单管理</a></li>
        </shiro:hasPermission>
        <li><a href="">物流管理</a></li>
    </shiro:hasAnyRoles>

</ul>

<a href="${pageContext.request.contextPath}/user/logout">退出</a>
</body>
</html>

5. 集成验证码

  1. 修改登录页面
    请输入验证码:<input type="text" name="code"/><img src="${pageContext.request.contextPath}/user/getImage">
  1. controller层开发
  @PostMapping("/login")
    public String login(String username,String password,String code,HttpSession session){
   
        String codes = (String) session.getAttribute("code");
            try {
   
                if(codes.equalsIgnoreCase(code)){
   
                    Subject subject = SecurityUtils.getSubject();
                subject.login(new UsernamePasswordToken(username,password));
                System.out.println("登陆成功");
                return "redirect:/index.jsp";
            }else{
   
                    throw new RuntimeException("验证码错误");
                }
            }catch (UnknownAccountException e){
   
                e.printStackTrace();
                System.out.println("用户名不存在");
            }catch (IncorrectCredentialsException e){
   
                e.printStackTrace();
                System.out.println("密码错误");
            }catch (RuntimeException e){
   
                e.printStackTrace();
                System.out.println(e.getMessage());
            }

        return "redirect:/login.jsp";
    }

    @RequestMapping("/getImage")
    public void getImage(HttpSession session, HttpServletResponse response){
   
        try {
   
            String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
            session.setAttribute("code",verifyCode);
            ServletOutputStream os = response.getOutputStream();
            //验证码存入图片
            response.setContentType("image/png");
            VerifyCodeUtils.outputImage(220, 60, os , verifyCode);
        } catch (IOException e) {
   
            e.printStackTrace();
        }

    }
  1. 需要重新修改salt的序列化规则

改用实现ByteSource 接口,因为SimpleByteSource 没有提供默认的无参构造,序列化会出现问题!

//自定义salt实现,实现序列化接口
public class MyByteSource implements ByteSource , Serializable {
   

    private  byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MyByteSource(){
   

    }

    public MyByteSource(byte[] bytes) {
   
        this.bytes = bytes;
    }

    public MyByteSource(char[] chars) {
   
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MyByteSource(String string) {
   
        this.bytes = CodecSupport.toBytes(string);
    }

    public MyByteSource(ByteSource source) {
   
        this.bytes = source.getBytes();
    }

    public MyByteSource(File file) {
   
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
    }

    public MyByteSource(InputStream stream) {
   
        this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
   
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
   
        return this.bytes;
    }

    public boolean isEmpty() {
   
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
   
        if (this.cachedHex == null) {
   
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
   
        if (this.cachedBase64 == null) {
   
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
   
        return this.toBase64();
    }

    public int hashCode() {
   
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
   
        if (o == this) {
   
            return true;
        } else if (o instanceof ByteSource) {
   
            ByteSource bs = (ByteSource)o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
   
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
   
        private BytesHelper() {
   
        }

        public byte[] getBytes(File file) {
   
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
   
            return this.toBytes(stream);
        }
    }
}

7. spring boot整合shiro之thymeleaf权限控制


在springboot整合Shiro的基础上,修改模板引擎为thymeleaf,需要注意的是thymeleaf界面的跳转要借助于控制器!

  1. 修改application.properties
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp

修改为

spring.thymeleaf.cache=false
spring.thymeleaf.suffix=.html
spring.thymeleaf.prefix=classpath:/templates/
  1. 添加springboot的thymeleaf的启动依赖和shiro整合thymeleaf
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>
  1. 修改JSP页面为html页面

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>登录页面</h2>
<form th:action="@{/user/login}" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    请输入验证码:<input type="text" name="code"/>
    <input type="submit" value="登录"><br></form>
<br><img th:src="@{/user/getImage}">

<a th:href="@{/user/registerview}">立即注册</a>
</form>
</body>
</html>

register.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http:www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h2>注册页面</h2>
<form th:action="@{/user/register}" method="post">
    账号:<input type="text" name="username"><br>
    密码:<input type="text" name="password"><br>
    <input type="submit" value="立即注册"><br>
</form>
</body>
</html>

index.html

<!DOCTYPE html >
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" xmlns:shrio="http://www.pollix.at/thymeleaf/shiro">
<head>
    <meta charset="UTF-8">
    <title>系统主页</title>
</head>
<body>
<h1>V0.1 系统主页</h1>

<h1><shiro:principal/></h1>
<ul>
    <shiro:hasAnyRoles name="admin,user">
        <li><a href="">用户管理</a>
            <ul>
                <ui>
                    <shiro:hasPermission name="user:create:*">
                        <li>创建</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:delete:01">
                        <li>删处</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:update:01">
                        <li>修改</li>
                    </shiro:hasPermission>
                    <shiro:hasPermission name="user:select:01">
                        <li>查询</li>
                    </shiro:hasPermission>
                </ui>
            </ul>
        </li>
    </shiro:hasAnyRoles>

    <shiro:hasAnyRoles name="admin,product,order">
        <shiro:hasPermission name="product:*:01">
            <li><a href="">商品管理</a></li>
        </shiro:hasPermission>
        <shiro:hasPermission name="order:*:01">
            <li><a href="">订单管理</a></li>
        </shiro:hasPermission>
        <li><a href="">物流管理</a></li>
    </shiro:hasAnyRoles>

</ul>

<hr>
<p><shiro:notAuthenticated>
</shiro:notAuthenticated></p>
<hr>
<p><shrio:authenticated/></p>

<a th:href="@{/user/logout}">退出</a>
</body>
</html>
  1. 修改web控制器
@Controller
@RequestMapping("/user")
public class UserController {
   

    @Autowired
    private UserService service;

    /** * 处理身份验证 * @param username * @param password * @return */
    @PostMapping("/login")
    public String login(String username,String password,String code,HttpSession session){
   
        String codes = (String) session.getAttribute("code");
            try {
   
                if(codes.equalsIgnoreCase(code)){
   
                    Subject subject = SecurityUtils.getSubject();
                subject.login(new UsernamePasswordToken(username,password));
                System.out.println("登陆成功");
                return "redirect:/user/index";
            }else{
   
                    throw new RuntimeException("验证码错误");
                }
            }catch (UnknownAccountException e){
   
                e.printStackTrace();
                System.out.println("用户名不存在");
            }catch (IncorrectCredentialsException e){
   
                e.printStackTrace();
                System.out.println("密码错误");
            }catch (RuntimeException e){
   
                e.printStackTrace();
                System.out.println(e.getMessage());
            }

        return "redirect:/user/loginview";
    }

    /** * 用户退出登录 * @return */
    @GetMapping("logout")
    public String logout(){
   
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "redirect:/user/loginview";
    }

    @PostMapping("/register")
    public String register(User user){
   
        try {
   
            service.save(user);
            System.out.println("注册成功");
            return "redirect:/user/loginview";
        }catch (Exception e){
   
            e.printStackTrace();
            System.out.println("注册失败");
            return "redirect:/user/registerview ";
        }

    }

    @RequestMapping("/getImage")
    public void getImage(HttpSession session, HttpServletResponse response){
   
        try {
   
            String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
            session.setAttribute("code",verifyCode);
            ServletOutputStream os = response.getOutputStream();
            //验证码存入图片
            response.setContentType("image/png");
            VerifyCodeUtils.outputImage(220, 60, os , verifyCode);
        } catch (IOException e) {
   
            e.printStackTrace();
        }

    }

    @RequestMapping("/loginview")
    public String loginview(){
   
        System.out.println("正在进入登录页面...");
        return "login";
    }

    @RequestMapping("/registerview")
    public String registerview(){
   
        System.out.println("正在进入注册页面...");
        return "register";
    }

    @RequestMapping("/index")
    public String index(){
   
        return "index";
    }
}

  1. 添加springboot方言处理,否则shiro标签将失效!

在shiroConfig.java中添加

    //加入spring boot的方言处理,否则页面标签将不起作用
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect(){
   
        return new ShiroDialect();
    }
  1. 测试

以xiaochen和zhangsan两种不同的身份登录系统!

xiaochen:

zhangsan:

比对数据库中xiaochen和zhansan对象的角色信息和权限

根据中间表可以知道xiaochen:admin,zhangsan:user,product

测试再通过角色权限表查看:

由此可见,虽然zhangsan拥有两个角色,但是他并没有拥有订单(order)权限,所以zhangsan没有订单管理选项!!

测试完成!!

参考学习:https://www.bilibili.com/video/BV1uz4y197Zm?p=26