SpringBoot系列十二:SpringBoot整合 Shiro
声明:本文来源于MLDN培训视频的课堂笔记,写在这里只是为了方便查阅。
1、概念:SpringBoot 整合 Shiro
2、具体内容
Shiro 是现在最为流行的权限认证开发框架,与它起名的只有最初的 SpringSecurity(这个开发框架非常不好用,但是千万不要 以为 SpringSecurity 没有用处,它在 SpringCloud 阶段将发挥重大的作用)。但是现在如果要想整合 Shiro 开发框架有一点很遗憾, SpringBoot 没有直接的配置支持,它不像整合所谓的 Kafka、Redis、DataSource,也就是说如果要想整合 Shiro 开发框架那么就必须 自己来进行配置。
2.1、项目开发准备
在整个的 Shiro 之中最为重要的部分:认证以及授权处理(Realm),在 Realm 里面实际上在开发之中所需要调用的业务方法 只有两类:根据用户编号取得用户的完整信息,在认证通过之后根据用户编号获得用户对应的所有的角色以及权限信息,而且既然已经到了微架构的阶段,那么不得不去面对一个问题,对于这种用户的业务操作是放在 WEB 端还是单独提出来做成一个 Rest 服务? 很明显,应该作为一个服务进行抽象出来,也就是说在整体的调用处理之中,Realm 需要进行 Rest 服务调用(RestTemplate 存在可 以让整个的调用更加容易)。
那么按照如上的设计方案,现在的整体的项目里面认为应该包含有如下的几个开发模块:
· microboot-shiro-api:应该提供有服务的 VO 类、各种加密处理的工具类;
· microboot-shiro-member-provider:进行用户认证与授权 REST 服务的提供,要暴露两个接口:用户信息获得、角色与权限信息获得;
· microboot-shiro-web:主要进行 Shiro 的认证与授权检测处理。
1、 【microboot-shiro-member-provider】保存本次的数据库脚本
-- 删除数据库 DROP DATABASE IF EXISTS study ; -- 创建数据库 CREATE DATABASE study CHARACTER SET UTF8 ; -- 使用数据库 USE study ; CREATE TABLE member( mid VARCHAR(50) , name VARCHAR(50) , password VARCHAR(32) , locked INT , CONSTRAINT pk_mid PRIMARY KEY(mid) ) ; CREATE TABLE role ( rid VARCHAR(50) , title VARCHAR(50) , CONSTRAINT pk_rid PRIMARY KEY(rid) ) ; CREATE TABLE action ( actid VARCHAR(50) , title VARCHAR(50) , rid VARCHAR(50) , CONSTRAINT pk_actid PRIMARY KEY(actid) ) ; CREATE TABLE member_role ( mid VARCHAR(50) , rid VARCHAR(50) ) ; INSERT INTO member(mid,name,password,locked) VALUES ('studyjava','study','2E866BF58289E01583AD418F486A69DF',0) ; INSERT INTO member(mid,name,password,locked) VALUES ('admin','admin','2E866BF58289E01583AD418F486A69DF',0) ; INSERT INTO role(rid,title) VALUES ('emp','雇员管理') ; INSERT INTO role(rid,title) VALUES ('dept','部门管理') ; INSERT INTO action(actid,title,rid) VALUES ('emp:add','雇员入职','emp') ; INSERT INTO action(actid,title,rid) VALUES ('emp:remove','雇员离职','emp') ; INSERT INTO action(actid,title,rid) VALUES ('emp:list','雇员列表','emp') ; INSERT INTO action(actid,title,rid) VALUES ('emp:edit','雇员编辑','emp') ; INSERT INTO action(actid,title,rid) VALUES ('dept:list','部门列表','dept') ; INSERT INTO action(actid,title,rid) VALUES ('dept:edit','部门编辑','dept') ; INSERT INTO member_role(mid,rid) VALUES ('studyjava','emp') ; INSERT INTO member_role(mid,rid) VALUES ('admin','emp') ; INSERT INTO member_role(mid,rid) VALUES ('admin','dept') ;
2、 【microboot-shiro-api】建立一个 Member 程序类,保存认证返回的信息;
· Shiro 进行认证处理的时候是要求根据一个用户的编号获得用户对应的完整信息,而后再进行用户是否存在的判断、密码 是否正确的判断、是否被锁定的判断。
package cn.study.vo; import java.io.Serializable; @SuppressWarnings("serial") public class Member implements Serializable { private String mid ; private String name ; private String password ; private Integer locked ; public String getMid() { return mid; } public void setMid(String mid) { this.mid = mid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getLocked() { return locked; } public void setLocked(Integer locked) { this.locked = locked; } @Override public String toString() { return "Member [mid=" + mid + ", name=" + name + ", password=" + password + ", locked=" + locked + "]"; } }
3、 【microboot-shiro-api】密码的加密处理;
package cn.study.util.enctype; public class MD5Code { /* * 下面这些S11-S44实际上是一个4*4的矩阵,在原始的C实现中是用#define 实现的, 这里把它们实现成为static * final是表示了只读,且能在同一个进程空间内的多个 Instance间共享 */ static final int S11 = 7; static final int S12 = 12; static final int S13 = 17; static final int S14 = 22; static final int S21 = 5; static final int S22 = 9; static final int S23 = 14; static final int S24 = 20; static final int S31 = 4; static final int S32 = 11; static final int S33 = 16; static final int S34 = 23; static final int S41 = 6; static final int S42 = 10; static final int S43 = 15; static final int S44 = 21; static final byte[] PADDING = { -128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; /* * 下面的三个成员是MD5计算过程中用到的3个核心数据,在原始的C实现中 被定义到MD5_CTX结构中 */ private long[] state = new long[4];// state (ABCD) private long[] count = new long[2];// number of bits, modulo 2^64 (lsb // first) private byte[] buffer = new byte[64]; // input buffer /* * digestHexStr是MD5的唯一一个公共成员,是最新一次计算结果的 16进制ASCII表示. */ public String digestHexStr; /* * digest,是最新一次计算结果的2进制内部表示,表示128bit的MD5值. */ private byte[] digest = new byte[16]; /* * getMD5ofStr是类MD5最主要的公共方法,入口参数是你想要进行MD5变换的字符串 * 返回的是变换完的结果,这个结果是从公共成员digestHexStr取得的. */ public String getMD5ofStr(String inbuf) { md5Init(); md5Update(inbuf.getBytes(), inbuf.length()); md5Final(); digestHexStr = ""; for (int i = 0; i < 16; i++) { digestHexStr += byteHEX(digest[i]); } return digestHexStr; } // 这是MD5这个类的标准构造函数,JavaBean要求有一个public的并且没有参数的构造函数 public MD5Code() { md5Init(); return; } /* md5Init是一个初始化函数,初始化核心变量,装入标准的幻数 */ private void md5Init() { count[0] = 0L; count[1] = 0L; // /* Load magic initialization constants. state[0] = 0x67452301L; state[1] = 0xefcdab89L; state[2] = 0x98badcfeL; state[3] = 0x10325476L; return; } /* * F, G, H ,I 是4个基本的MD5函数,在原始的MD5的C实现中,由于它们是 * 简单的位运算,可能出于效率的考虑把它们实现成了宏,在java中,我们把它们 实现成了private方法,名字保持了原来C中的。 */ private long F(long x, long y, long z) { return (x & y) | ((~x) & z); } private long G(long x, long y, long z) { return (x & z) | (y & (~z)); } private long H(long x, long y, long z) { return x ^ y ^ z; } private long I(long x, long y, long z) { return y ^ (x | (~z)); } /* * FF,GG,HH和II将调用F,G,H,I进行近一步变换 FF, GG, HH, and II transformations for * rounds 1, 2, 3, and 4. Rotation is separate from addition to prevent * recomputation. */ private long FF(long a, long b, long c, long d, long x, long s, long ac) { a += F(b, c, d) + x + ac; a = ((int) a << s) | ((int) a >>> (32 - s)); a += b; return a; } private long GG(long a, long b, long c, long d, long x, long s, long ac) { a += G(b, c, d) + x + ac; a = ((int) a << s) | ((int) a >>> (32 - s)); a += b; return a; } private long HH(long a, long b, long c, long d, long x, long s, long ac) { a += H(b, c, d) + x + ac; a = ((int) a << s) | ((int) a >>> (32 - s)); a += b; return a; } private long II(long a, long b, long c, long d, long x, long s, long ac) { a += I(b, c, d) + x + ac; a = ((int) a << s) | ((int) a >>> (32 - s)); a += b; return a; } /* * md5Update是MD5的主计算过程,inbuf是要变换的字节串,inputlen是长度,这个 * 函数由getMD5ofStr调用,调用之前需要调用md5init,因此把它设计成private的 */ private void md5Update(byte[] inbuf, int inputLen) { int i, index, partLen; byte[] block = new byte[64]; index = (int) (count[0] >>> 3) & 0x3F; // /* Update number of bits */ if ((count[0] += (inputLen << 3)) < (inputLen << 3)) count[1]++; count[1] += (inputLen >>> 29); partLen = 64 - index; // Transform as many times as possible. if (inputLen >= partLen) { md5Memcpy(buffer, inbuf, index, 0, partLen); md5Transform(buffer); for (i = partLen; i + 63 < inputLen; i += 64) { md5Memcpy(block, inbuf, 0, i, 64); md5Transform(block); } index = 0; } else i = 0; // /* Buffer remaining input */ md5Memcpy(buffer, inbuf, index, i, inputLen - i); } /* * md5Final整理和填写输出结果 */ private void md5Final() { byte[] bits = new byte[8]; int index, padLen; // /* Save number of bits */ Encode(bits, count, 8); // /* Pad out to 56 mod 64. index = (int) (count[0] >>> 3) & 0x3f; padLen = (index < 56) ? (56 - index) : (120 - index); md5Update(PADDING, padLen); // /* Append length (before padding) */ md5Update(bits, 8); // /* Store state in digest */ Encode(digest, state, 16); } /* * md5Memcpy是一个内部使用的byte数组的块拷贝函数,从input的inpos开始把len长度的 * 字节拷贝到output的outpos位置开始 */ private void md5Memcpy(byte[] output, byte[] input, int outpos, int inpos, int len) { int i; for (i = 0; i < len; i++) output[outpos + i] = input[inpos + i]; } /* * md5Transform是MD5核心变换程序,有md5Update调用,block是分块的原始字节 */ private void md5Transform(byte block[]) { long a = state[0], b = state[1], c = state[2], d = state[3]; long[] x = new long[16]; Decode(x, block, 64); /* Round 1 */ a = FF(a, b, c, d, x[0], S11, 0xd76aa478L); /* 1 */ d = FF(d, a, b, c, x[1], S12, 0xe8c7b756L); /* 2 */ c = FF(c, d, a, b, x[2], S13, 0x242070dbL); /* 3 */ b = FF(b, c, d, a, x[3], S14, 0xc1bdceeeL); /* 4 */ a = FF(a, b, c, d, x[4], S11, 0xf57c0fafL); /* 5 */ d = FF(d, a, b, c, x[5], S12, 0x4787c62aL); /* 6 */ c = FF(c, d, a, b, x[6], S13, 0xa8304613L); /* 7 */ b = FF(b, c, d, a, x[7], S14, 0xfd469501L); /* 8 */ a = FF(a, b, c, d, x[8], S11, 0x698098d8L); /* 9 */ d = FF(d, a, b, c, x[9], S12, 0x8b44f7afL); /* 10 */ c = FF(c, d, a, b, x[10], S13, 0xffff5bb1L); /* 11 */ b = FF(b, c, d, a, x[11], S14, 0x895cd7beL); /* 12 */ a = FF(a, b, c, d, x[12], S11, 0x6b901122L); /* 13 */ d = FF(d, a, b, c, x[13], S12, 0xfd987193L); /* 14 */ c = FF(c, d, a, b, x[14], S13, 0xa679438eL); /* 15 */ b = FF(b, c, d, a, x[15], S14, 0x49b40821L); /* 16 */ /* Round 2 */ a = GG(a, b, c, d, x[1], S21, 0xf61e2562L); /* 17 */ d = GG(d, a, b, c, x[6], S22, 0xc040b340L); /* 18 */ c = GG(c, d, a, b, x[11], S23, 0x265e5a51L); /* 19 */ b = GG(b, c, d, a, x[0], S24, 0xe9b6c7aaL); /* 20 */ a = GG(a, b, c, d, x[5], S21, 0xd62f105dL); /* 21 */ d = GG(d, a, b, c, x[10], S22, 0x2441453L); /* 22 */ c = GG(c, d, a, b, x[15], S23, 0xd8a1e681L); /* 23 */ b = GG(b, c, d, a, x[4], S24, 0xe7d3fbc8L); /* 24 */ a = GG(a, b, c, d, x[9], S21, 0x21e1cde6L); /* 25 */ d = GG(d, a, b, c, x[14], S22, 0xc33707d6L); /* 26 */ c = GG(c, d, a, b, x[3], S23, 0xf4d50d87L); /* 27 */ b = GG(b, c, d, a, x[8], S24, 0x455a14edL); /* 28 */ a = GG(a, b, c, d, x[13], S21, 0xa9e3e905L); /* 29 */ d = GG(d, a, b, c, x[2], S22, 0xfcefa3f8L); /* 30 */ c = GG(c, d, a, b, x[7], S23, 0x676f02d9L); /* 31 */ b = GG(b, c, d, a, x[12], S24, 0x8d2a4c8aL); /* 32 */ /* Round 3 */ a = HH(a, b, c, d, x[5], S31, 0xfffa3942L); /* 33 */ d = HH(d, a, b, c, x[8], S32, 0x8771f681L); /* 34 */ c = HH(c, d, a, b, x[11], S33, 0x6d9d6122L); /* 35 */ b = HH(b, c, d, a, x[14], S34, 0xfde5380cL); /* 36 */ a = HH(a, b, c, d, x[1], S31, 0xa4beea44L); /* 37 */ d = HH(d, a, b, c, x[4], S32, 0x4bdecfa9L); /* 38 */ c = HH(c, d, a, b, x[7], S33, 0xf6bb4b60L); /* 39 */ b = HH(b, c, d, a, x[10], S34, 0xbebfbc70L); /* 40 */ a = HH(a, b, c, d, x[13], S31, 0x289b7ec6L); /* 41 */ d = HH(d, a, b, c, x[0], S32, 0xeaa127faL); /* 42 */ c = HH(c, d, a, b, x[3], S33, 0xd4ef3085L); /* 43 */ b = HH(b, c, d, a, x[6], S34, 0x4881d05L); /* 44 */ a = HH(a, b, c, d, x[9], S31, 0xd9d4d039L); /* 45 */ d = HH(d, a, b, c, x[12], S32, 0xe6db99e5L); /* 46 */ c = HH(c, d, a, b, x[15], S33, 0x1fa27cf8L); /* 47 */ b = HH(b, c, d, a, x[2], S34, 0xc4ac5665L); /* 48 */ /* Round 4 */ a = II(a, b, c, d, x[0], S41, 0xf4292244L); /* 49 */ d = II(d, a, b, c, x[7], S42, 0x432aff97L); /* 50 */ c = II(c, d, a, b, x[14], S43, 0xab9423a7L); /* 51 */ b = II(b, c, d, a, x[5], S44, 0xfc93a039L); /* 52 */ a = II(a, b, c, d, x[12], S41, 0x655b59c3L); /* 53 */ d = II(d, a, b, c, x[3], S42, 0x8f0ccc92L); /* 54 */ c = II(c, d, a, b, x[10], S43, 0xffeff47dL); /* 55 */ b = II(b, c, d, a, x[1], S44, 0x85845dd1L); /* 56 */ a = II(a, b, c, d, x[8], S41, 0x6fa87e4fL); /* 57 */ d = II(d, a, b, c, x[15], S42, 0xfe2ce6e0L); /* 58 */ c = II(c, d, a, b, x[6], S43, 0xa3014314L); /* 59 */ b = II(b, c, d, a, x[13], S44, 0x4e0811a1L); /* 60 */ a = II(a, b, c, d, x[4], S41, 0xf7537e82L); /* 61 */ d = II(d, a, b, c, x[11], S42, 0xbd3af235L); /* 62 */ c = II(c, d, a, b, x[2], S43, 0x2ad7d2bbL); /* 63 */ b = II(b, c, d, a, x[9], S44, 0xeb86d391L); /* 64 */ state[0] += a; state[1] += b; state[2] += c; state[3] += d; } /* * Encode把long数组按顺序拆成byte数组,因为java的long类型是64bit的, 只拆低32bit,以适应原始C实现的用途 */ private void Encode(byte[] output, long[] input, int len) { int i, j; for (i = 0, j = 0; j < len; i++, j += 4) { output[j] = (byte) (input[i] & 0xffL); output[j + 1] = (byte) ((input[i] >>> 8) & 0xffL); output[j + 2] = (byte) ((input[i] >>> 16) & 0xffL); output[j + 3] = (byte) ((input[i] >>> 24) & 0xffL); } } /* * Decode把byte数组按顺序合成成long数组,因为java的long类型是64bit的, * 只合成低32bit,高32bit清零,以适应原始C实现的用途 */ private void Decode(long[] output, byte[] input, int len) { int i, j; for (i = 0, j = 0; j < len; i++, j += 4) output[i] = b2iu(input[j]) | (b2iu(input[j + 1]) << 8) | (b2iu(input[j + 2]) << 16) | (b2iu(input[j + 3]) << 24); return; } /* * b2iu是我写的一个把byte按照不考虑正负号的原则的"升位"程序,因为java没有unsigned运算 */ public static long b2iu(byte b) { return b < 0 ? b & 0x7F + 128 : b; } /* * byteHEX(),用来把一个byte类型的数转换成十六进制的ASCII表示, * 因为java中的byte的toString无法实现这一点,我们又没有C语言中的 sprintf(outbuf,"%02X",ib) */ public static String byteHEX(byte ib) { char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; char[] ob = new char[2]; ob[0] = Digit[(ib >>> 4) & 0X0F]; ob[1] = Digit[ib & 0X0F]; String s = new String(ob); return s; } }
package cn.study.util.enctype; import java.util.Base64; public class PasswordUtil { private static final String SEED = "studyjava" ; // 该数据为种子数,如果要加密则需要使用Base64做多次迭代 private static final int NE_NUM = 3 ; // 密码迭代处理3次 private PasswordUtil() {} private static String createSeed() { // 创建一个基于Base64的种子数 String str = SEED ; for (int x = 0 ; x < NE_NUM ; x ++) { str = Base64.getEncoder().encodeToString(str.getBytes()) ; } return str ; } /** * 进行密码的处理操作 * @param password 用户输入的真实密码 * @return 与数据库保存匹配的加密的处理密码 */ public static String getPassword(String password) { MD5Code md5 = new MD5Code() ; String pass = "{" + password + ":" + createSeed() + "}"; for (int x = 0 ; x < NE_NUM ; x ++) { pass = md5.getMD5ofStr(pass) ; } return pass ; } }
2.2、用户微服务
所谓的用户微服务指的是要求在“microboot-shiro-member-provider”里面进行实现,该服务之中需要考虑如下的几点:
· 该服务需要进行数据库的开发,所以一定要进行数据库连接池的配置;
· 既然要进行微服务的编写,那么就一定需要提供有业务接口以及 DAO 实现子类,现在的实现将依靠 MyBatis 完成;
· 所有的微服务最终要通过控制器的 Rest 进行发布处理。
1、 【microboot-shiro-member-provider】配置 Druid 数据库连接池;
· 需要修改 pom.xml 配置文件,为项目的整合添加相关的支持包:
<dependency> <groupId>cn.mldn</groupId> <artifactId>microboot-shiro-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency>
2、 【microboot-shiro-member-provider】建立几个 DAO 接口:
· 提供用户认证的 DAO 接口:IMemberDAO;
package cn.study.microboot.dao; import org.apache.ibatis.annotations.Mapper; import cn.study.vo.Member; @Mapper public interface IMemberDAO { public Member findById(String mid) ; }
· 提供角色检测的 IRoleDAO 接口:
package cn.study.microboot.dao; import java.util.Set; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IRoleDAO { public Set<String> findAllRoleByMember(String mid) ; }
· 提供所有权限检测的 IActionDAO 接口:
package cn.study.microboot.dao; import java.util.Set; import org.apache.ibatis.annotations.Mapper; @Mapper public interface IActionDAO { public Set<String> findAllActionByMember(String mid) ; }
3、 【microboot-shiro-member-provider】将 mybatis 的配置文件拷贝到项目的“src/main/resources”中:
· src/main/resources/mybatis/mybatis.cfg.xml 文件配置:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 进行Mybatis的相应的环境的属性定义 --> <settings> <!-- 在本项目之中开启二级缓存 --> <setting name="cacheEnabled" value="true"/> </settings> </configuration>
· 配置 src/main/resources/mybatis/mapper/cn/mldn/Member.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="cn.study.microboot.dao.IMemberDAO"> <select id="findById" parameterType="String" resultType="Member"> SELECT mid,name,password,locked FROM member WHERE mid=#{mid} ; </select> </mapper>
· 配置 src/main/resources/mybatis/mapper/cn/mldn/Role.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="cn.study.microboot.dao.IRoleDAO"> <select id="findAllRoleByMember" parameterType="String" resultType="String"> SELECT rid FROM role WHERE rid IN ( SELECT rid FROM member_role WHERE mid=#{mid}) ; </select> </mapper>
· 配置 src/main/resources/mybatis/mapper/cn/mldn/Action.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="cn.study.microboot.dao.IActionDAO"> <select id="findAllActionByMember" parameterType="String" resultType="String"> SELECT actid FROM action WHERE rid IN ( SELECT rid FROM member_role WHERE mid=#{mid}) ; </select> </mapper>
4、 【microboot-shiro-member-provider】修改 application.yml 配置文件:
server:
port: 8001
mybatis:
config-location: classpath:mybatis/mybatis.cfg.xml # mybatis配置文件所在路径
type-aliases-package: cn.study.vo # 定义所有操作类的别名所在包
mapper-locations: # 所有的mapper映射文件
- classpath:mybatis/mapper/**/*.xml
spring:
messages:
basename: i18n/Messages,i18n/Pages
datasource:
type: com.alibaba.druid.pool.DruidDataSource # 配置当前要使用的数据源的操作类型
driver-class-name: org.gjt.mm.mysql.Driver # 配置MySQL的驱动程序类
url: jdbc:mysql://localhost:3306/study # 数据库连接地址
username: root # 数据库用户名
password: mysqladmin # 数据库连接密码
dbcp2: # 进行数据库连接池的配置
min-idle: 5 # 数据库连接池的最小维持连接数
initial-size: 5 # 初始化提供的连接数
max-total: 5 # 最大的连接数
max-wait-millis: 200 # 等待连接获取的最大超时时间
5、 【microboot-shiro-member-provider】定义 IMemberService 业务接口:
package cn.study.microboot.service; import java.util.Map; import java.util.Set; import cn.study.vo.Member; public interface IMemberService { public Member get(String mid) ; public Map<String,Set<String>> listAuthByMember(String mid) ; }
package cn.study.microboot.service.impl; import java.util.HashMap; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import org.springframework.stereotype.Service; import cn.study.microboot.dao.IActionDAO; import cn.study.microboot.dao.IMemberDAO; import cn.study.microboot.dao.IRoleDAO; import cn.study.microboot.service.IMemberService; import cn.study.vo.Member; @Service public class MemberServiceImpl implements IMemberService { @Resource private IMemberDAO memberDAO; @Resource private IRoleDAO roleDAO; @Resource private IActionDAO actionDAO; @Override public Member get(String mid) { return this.memberDAO.findById(mid); } @Override public Map<String, Set<String>> listAuthByMember(String mid) { Map<String, Set<String>> map = new HashMap<String, Set<String>>(); map.put("allRoles", this.roleDAO.findAllRoleByMember(mid)); map.put("allActions", this.actionDAO.findAllActionByMember(mid)); return map; } }
6、 【microboot-shiro-member-provider】编写业务层功能测试类;
package cn.study.microboot; import javax.annotation.Resource; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import cn.study.microboot.service.IMemberService; @SpringBootTest(classes = StartSpringBootMain.class) @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration public class TestMemberService { @Resource private IMemberService memberService ; @Test public void testGet() { System.out.println(this.memberService.get("admin")); } @Test public void testAuth() { System.out.println(this.memberService.listAuthByMember("admin")); } }
7、 【microboot-shiro-member-provider】进行控制层编写,控制层现在给出的一定是 Rest 服务:
package cn.study.microboot.controller; import javax.annotation.Resource; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import cn.study.microboot.service.IMemberService; @RestController public class MemberController { @Resource private IMemberService memberService; @RequestMapping(value="/member/get",method=RequestMethod.POST) public Object get(String mid) { return this.memberService.get(mid) ; } @RequestMapping(value="/member/auth",method=RequestMethod.POST) public Object auth(String mid) { return this.memberService.listAuthByMember(mid) ; } }
认证服务端口:http://localhost:8001/member/get?mid=admin;
授权服务端口:http://localhost:8001/member/auth?mid=admin;
8、 【microboot-shiro-member-provider】编写控制层测试,如果要访问 Rest 服务肯定要使用 RestTemplate 完成,这个类现在为了 简单起见,直接进行对象实例化处理:
package cn.study.microboot; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.web.client.RestTemplate; import cn.study.vo.Member; @SpringBootTest(classes = StartSpringBootMain.class) @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration public class TestMemberController { private RestTemplate restTemplate = new RestTemplate() ; @Test public void testGet() { String url = "http://localhost:8001/member/get?mid=admin" ; Member vo = this.restTemplate.postForObject(url, null, Member.class) ; System.out.println(vo); } @SuppressWarnings("unchecked") @Test public void testAuth() { String url = "http://localhost:8001/member/auth?mid=admin" ; Map<String,Object> map = this.restTemplate.postForObject(url, null, Map.class) ; Set<String> allRoles = new HashSet<String>() ; Set<String> allActions = new HashSet<String>() ; allRoles.addAll((List<String>) map.get("allRoles")); allActions.addAll((List<String>) map.get("allActions")) ; System.out.println("【角色】" + allRoles); System.out.println("【权限】" + allActions); } }
那么此时一个专门进行用户认证以及授权检测的微服务开发完成。
2.3、定义 Shiro 整合服务
在本次项目之中 WEB 模块为“microboot-shiro-web”,很明显对于 WEB 模块之中必须要求调用用户认证与授权微服务(Realm), 而后需要进行各种依赖包的配置(Shiro)、考虑到各种缓存的问题、认证与授权检测问题。
1、 【microboot-shiro-web】修改 pom.xml 配置文件,追加 Shiro 的相关依赖程序包:
<dependency> <groupId>cn.study</groupId> <artifactId>microboot-shiro-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-quartz</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2、 【microboot-shiro-web】建立一个 RestTemplate 的配置类对象:
package cn.study.microboot.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class RestConfig { @Bean public RestTemplate getRestTemplate() { return new RestTemplate() ; } }
3、 【microboot-shiro-web】Shiro 之中所有认证与授权的处理都在 Realm 之中定义了;
package cn.study.microboot.realm; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Resource; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; 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.springframework.web.client.RestTemplate; import cn.study.util.enctype.PasswordUtil; import cn.study.vo.Member; public class MemberRealm extends AuthorizingRealm { @Resource private RestTemplate restTemplate ; @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { System.out.println("============== 1、进行认证操作处理 =============="); String mid = token.getPrincipal().toString(); // 用户名 // 取得用户名之后就需要通过业务层获取用户对象以确定改用户名是否可用 String url = "http://localhost:8001/member/get?mid=" + mid ; Member member = this.restTemplate.postForObject(url, null, Member.class) ; // 通过用户名获取用户信息 if (member == null) { // 表示该用户信息不存在,不存在则应该抛出一个异常 throw new UnknownAccountException("搞什么搞,用户名不存在!"); } // 用户名如果存在了,那么就需要确定密码是否正确 String password = PasswordUtil .getPassword(new String((char[]) token.getCredentials())); if (!password.equals(member.getPassword())) { // 密码验证 throw new IncorrectCredentialsException("密码都记不住,去死吧!"); } // 随后还需要考虑用户被锁定的问题 if (member.getLocked().equals(1)) { // 1表示非0,非0就是true throw new LockedAccountException("被锁了,求解锁去吧!"); } // 定义需要进行返回的操作数据信息项,返回的认证信息使用应该是密文 SimpleAuthenticationInfo auth = new SimpleAuthenticationInfo( token.getPrincipal(), password, "memberRealm"); // 在认证完成之后可以直接取得用户所需要的信息内容,保存在Session之中 SecurityUtils.getSubject().getSession().setAttribute("name", "我的名字"); return auth; } @SuppressWarnings("unchecked") @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { System.out.println("++++++++++++++ 2、进行授权操作处理 ++++++++++++++"); // 该操作的主要目的是取得授权信息,说的直白一点就是角色和权限数据 SimpleAuthorizationInfo auth = new SimpleAuthorizationInfo(); // 执行到此方法的时候一定是已经进行过用户认证处理了(用户名和密码一定是正确的) String mid = (String) principals.getPrimaryPrincipal(); // 取得用户名 String url = "http://localhost:8001/member/auth?mid=" + mid ; Map<String,Object> map = this.restTemplate.postForObject(url, null, Map.class) ; Set<String> allRoles = new HashSet<String>() ; Set<String> allActions = new HashSet<String>() ; allRoles.addAll((List<String>) map.get("allRoles")); allActions.addAll((List<String>) map.get("allActions")) ; auth.setRoles(allRoles); // 保存所有的角色 auth.setStringPermissions(allActions); // 保存所有的权限 return auth; } }
4、 【microboot-shiro-web】现在虽然准备好了 Realm 程序类,但是在整个 Shiro 进行整合处理的时候实际上需要编写大量的配置 程序类,所以这个时候如果直接使用 xml 配置文件虽然可以,但是不标准,最好的做法是你将所有的 xml 配置项变为 Bean 配置。
package cn.study.microboot.config; import java.util.HashMap; import java.util.Map; import javax.servlet.Filter; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.RememberMeManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.session.mgt.eis.SessionIdGenerator; import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler; 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.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.filter.authc.LogoutFilter; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import cn.study.microboot.realm.CustomerCredentialsMatcher; import cn.study.microboot.realm.MemberRealm; @Configuration public class ShiroConfig { @Bean public MemberRealm getRealm() {// 1、获取配置的Realm,之所以没使用注解配置,是因为此处需要考虑到加密处理 MemberRealm realm = new MemberRealm(); realm.setCredentialsMatcher(new CustomerCredentialsMatcher()); return realm; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); daap.setProxyTargetClass(true); return daap; } @Bean public EhCacheManager getCacheManager() {// 2、缓存配置 EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); return cacheManager; } @Bean public SessionIdGenerator getSessionIdGenerator() { // 3 return new JavaUuidSessionIdGenerator(); } @Bean public SessionDAO getSessionDAO(SessionIdGenerator sessionIdGenerator) { // 4 EnterpriseCacheSessionDAO sessionDAO = new EnterpriseCacheSessionDAO(); sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); sessionDAO.setSessionIdGenerator(sessionIdGenerator); return sessionDAO; } @Bean public RememberMeManager getRememberManager() { // 5 CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); SimpleCookie cookie = new SimpleCookie("studyJAVA-RememberMe"); cookie.setHttpOnly(true); cookie.setMaxAge(3600); rememberMeManager.setCookie(cookie); return rememberMeManager; } @Bean public QuartzSessionValidationScheduler getQuartzSessionValidationScheduler() { QuartzSessionValidationScheduler sessionValidationScheduler = new QuartzSessionValidationScheduler(); sessionValidationScheduler.setSessionValidationInterval(100000); return sessionValidationScheduler; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); aasa.setSecurityManager(securityManager); return aasa; } @Bean public DefaultWebSessionManager getSessionManager(SessionDAO sessionDAO, QuartzSessionValidationScheduler sessionValidationScheduler) { // 6 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(1000000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionValidationScheduler(sessionValidationScheduler); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setSessionDAO(sessionDAO); SimpleCookie sessionIdCookie = new SimpleCookie("study-session-id"); sessionIdCookie.setHttpOnly(true); sessionIdCookie.setMaxAge(-1); sessionManager.setSessionIdCookie(sessionIdCookie); sessionManager.setSessionIdCookieEnabled(true); return sessionManager; } @Bean public DefaultWebSecurityManager getSecurityManager(Realm memberRealm, EhCacheManager cacheManager, SessionManager sessionManager, RememberMeManager rememberMeManager) {// 7 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(memberRealm); securityManager.setCacheManager(cacheManager); securityManager.setSessionManager(sessionManager); securityManager.setRememberMeManager(rememberMeManager); return securityManager; } public FormAuthenticationFilter getLoginFilter() { // 在ShiroFilterFactoryBean中使用 FormAuthenticationFilter filter = new FormAuthenticationFilter(); filter.setUsernameParam("mid"); filter.setPasswordParam("password"); filter.setRememberMeParam("rememberMe"); filter.setLoginUrl("/loginPage"); // 登录提交页面 filter.setFailureKeyAttribute("error"); return filter; } public LogoutFilter getLogoutFilter() { // 在ShiroFilterFactoryBean中使用 LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setRedirectUrl("/"); // 首页路径,登录注销后回到的页面 return logoutFilter; } @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/loginPage"); // 设置登录页路径 shiroFilterFactoryBean.setSuccessUrl("/pages/hello"); // 设置跳转成功页 shiroFilterFactoryBean.setUnauthorizedUrl("/pages/unauthUrl"); // 授权错误页 Map<String, Filter> filters = new HashMap<String, Filter>(); filters.put("authc", this.getLoginFilter()); filters.put("logout", this.getLogoutFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterChainDefinitionMap = new HashMap<String, String>(); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/loginPage", "authc"); // 定义内置登录处理 filterChainDefinitionMap.put("/pages/back/**", "authc"); filterChainDefinitionMap.put("/*", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } }
在src/main/resources 目录之中编写ehcache.xml 配置文件;
<?xml version="1.1" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir"/> <defaultCache maxElementsInMemory="2000" eternal="true" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"/> <!-- <cache name="diskCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> --> <cache name="passwordRetryCache" maxElementsInMemory="2000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="0" overflowToDisk="false"> </cache> <cache name="authorizationCache" maxElementsInMemory="2000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="0" overflowToDisk="false"> </cache> <cache name="authenticationCache" maxElementsInMemory="2000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="0" overflowToDisk="false"> </cache> <cache name="shiro-activeSessionCache" maxElementsInMemory="2000" eternal="false" timeToIdleSeconds="1800" timeToLiveSeconds="0" overflowToDisk="false"> </cache> </ehcache>
5、 【microboot-shiro-web】建立一个控制器
package cn.study.microboot.controller; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DeptController { @RequiresRoles("dept") @RequestMapping("/pages/back/dept/get") public String get() { return "部门信息" ; } }
6、 【microboot-shiro-web】登录出现了错误之后应该跑到表单上,所以建立一个 MemberController,这个程序类负责此跳转处理
package cn.study.microboot.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MemberController { @RequestMapping({"/loginPage"}) public String get() { return "member_login"; } }
7、 【microboot-shiro-web】建立一个 templates/member_login.html 的页面;
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>SpringBoot模版渲染</title> <script type="text/javascript" th:src="@{/js/main.js}"></script> <link rel="icon" type="image/x-icon" href="/images/study.ico"/> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> </head> <body> <h1>用户登录表单、<span th:text="${error}"/></h1> <form th:action="@{/loginPage}" method="post"> 登录名:<input type="text" name="mid" value="studyjava"/><br/> 密 码:<input type="text" name="password" value="hello"/><br/> <input type="submit" value="登录"/> </form> </body> </html>
此时实现了一个最基础的整合处理操作。
2.4、使用 Redis 进行数据缓存
现在是使用了 EHCache 缓存组件进行了缓存处理,而实际的项目之中往往会利用 Redis 实现缓存配置,那么下面将对程序进 行一些修改。
1、 【microboot-shiro-web】如果要进行缓存的使用,则首先一定要配置缓存处理类;
package cn.study.microboot.cache; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; public class RedisCache<K, V> implements Cache<K, V> { private Log log = LogFactory.getLog(RedisCache.class); private RedisTemplate<String, Object> redisTempate; // 要提供有Redis处理工具类 public RedisCache(RedisTemplate<String, Object> redisTempate) { this.redisTempate = redisTempate; } @Override public V get(K key) throws CacheException { log.info("### get() : K = " + key); return (V) this.redisTempate.opsForValue().get(key.toString()); } @Override public V put(K key, V value) throws CacheException { log.info("### put() : K = " + key + "、V = " + value); this.redisTempate.opsForValue().set(key.toString(), value); return value; } @Override public V remove(K key) throws CacheException { log.info("### remove() : K = " + key); V val = this.get(key); this.redisTempate.delete(key.toString()); return val; } @Override public void clear() throws CacheException { log.info("### clear()"); this.redisTempate.execute(new RedisCallback<Boolean>() { @Override public Boolean doInRedis(RedisConnection connection) throws DataAccessException { connection.flushDb(); // 清空数据库 return true; } }); } @Override public int size() { log.info("### size()"); return this.redisTempate.execute(new RedisCallback<Integer>() { @Override public Integer doInRedis(RedisConnection connection) throws DataAccessException { return connection.keys("*".getBytes()).size(); } }); } @Override public Set<K> keys() { log.info("### keys()"); return this.redisTempate.execute(new RedisCallback<Set<K>>() { @Override public Set<K> doInRedis(RedisConnection connection) throws DataAccessException { Set<K> set = new HashSet<K>(); Set<byte[]> keys = connection.keys("*".getBytes()); Iterator<byte[]> iter = keys.iterator(); while (iter.hasNext()) { set.add((K) iter.next()); } return set; } }); } @Override public Collection<V> values() { log.info("### values()"); return this.redisTempate.execute(new RedisCallback<Set<V>>() { @Override public Set<V> doInRedis(RedisConnection connection) throws DataAccessException { Set<V> set = new HashSet<V>(); Set<byte[]> keys = connection.keys("*".getBytes()); Iterator<byte[]> iter = keys.iterator(); while (iter.hasNext()) { set.add((V) connection.get(iter.next())); } return set; } }); } }
2、 【microboot-shiro-web】进行 Redis 缓存管理类的配置
package cn.study.microboot.cache; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Resource; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; @Component public class RedisCacheManager implements CacheManager { // CacheManager负责所有数据的缓存,那么对于数据而言,应该保存在缓存里面 private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<String, Cache>(); @Resource private RedisTemplate<String, Object> redisTemplate; @Override public Cache<Object, Object> getCache(String name) throws CacheException { Cache<Object, Object> cache = this.caches.get(name); // 通过Map取得cache数据 if (cache == null) { // 当前的集合里面没有Cache的数据 cache = new RedisCache(this.redisTemplate); // 实例化一个新的Cache对象 this.caches.put(name, cache); } return cache; } }
3、 【microboot-shiro-web】配置一个 Shiro 中的 Session 管理操作
package cn.study.microboot.session; import java.io.Serializable; import javax.annotation.Resource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; // 此时的类将实现SessionDAO的改写 import org.springframework.data.redis.core.RedisTemplate; public class RedisSessionDAO extends EnterpriseCacheSessionDAO { private Log log = LogFactory.getLog(RedisSessionDAO.class); @Resource private RedisTemplate<String, Object> redisTempate; // 要提供有Redis处理工具类 @Override protected Serializable doCreate(Session session) { // 创建Session,返回session id log.info("*** doCreate : " + session); Serializable sessionId = super.doCreate(session); // 创建sessionid // 将当前创建好的Session的数据保存在Redis数据库里面 this.redisTempate.opsForValue().set(sessionId.toString(), session, 1800); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { // 根据session // id读取session数据 log.info("*** doReadSession : " + sessionId); Session session = super.doReadSession(sessionId); // 读取Session数据 if (session == null) { // 现在没有读取到session数据,通过Redis读取 return (Session) this.redisTempate.opsForValue() .get(sessionId.toString()); } return null; } @Override protected void doUpdate(Session session) { // 实现Session更新,每次操作都要更新 log.info("*** doUpdate : " + session); super.doUpdate(session); if (session != null) { this.redisTempate.opsForValue().set(session.getId().toString(), session, 1800); } } @Override protected void doDelete(Session session) { // session的删除处理 log.info("*** doDelete : " + session); super.doDelete(session); this.redisTempate.delete(session.getId().toString()); } }
4、 【microboot-shiro-web】在当前的项目开发过程之中,配置 Shiro 的 Bean 里面所使用的还是 EHCache 缓存组件,所以需要进 行更换处理。
· 更换现在要使用的 SessionDAO 实现子类:
· 更换使用的缓存组件:
1 package cn.mldn.microboot.config; 2 import java.util.HashMap; 3 import java.util.Map; 4 5 import javax.servlet.Filter; 6 7 import org.apache.shiro.mgt.RememberMeManager; 8 import org.apache.shiro.realm.Realm; 9 import org.apache.shiro.session.mgt.SessionManager; 10 import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; 11 import org.apache.shiro.session.mgt.eis.SessionDAO; 12 import org.apache.shiro.session.mgt.eis.SessionIdGenerator; 13 import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler; 14 import org.apache.shiro.spring.LifecycleBeanPostProcessor; 15 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; 16 import org.apache.shiro.spring.web.ShiroFilterFactoryBean; 17 import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; 18 import org.apache.shiro.web.filter.authc.LogoutFilter; 19 import org.apache.shiro.web.mgt.CookieRememberMeManager; 20 import org.apache.shiro.web.mgt.DefaultWebSecurityManager; 21 import org.apache.shiro.web.servlet.SimpleCookie; 22 import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; 23 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; 24 import org.springframework.context.annotation.Bean; 25 import org.springframework.context.annotation.Configuration; 26 import org.springframework.context.annotation.DependsOn; 27 28 import cn.mldn.microboot.cache.RedisCacheManager; 29 import cn.mldn.microboot.realm.CustomerCredentialsMatcher; 30 import cn.mldn.microboot.realm.MemberRealm; 31 import cn.mldn.microboot.session.RedisSessionDAO; 32 33 @Configuration 34 public class ShiroConfig { 35 @Bean 36 public MemberRealm getRealm() {// 1、获取配置的Realm,之所以没使用注解配置,是因为此处需要考虑到加密处理 37 MemberRealm realm = new MemberRealm(); 38 realm.setCredentialsMatcher(new CustomerCredentialsMatcher()); 39 return realm; 40 } 41 42 @Bean(name = "lifecycleBeanPostProcessor") 43 public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { 44 return new LifecycleBeanPostProcessor(); 45 } 46 47 @Bean 48 @DependsOn("lifecycleBeanPostProcessor") 49 public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { 50 DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); 51 daap.setProxyTargetClass(true); 52 return daap; 53 } 54 55 // @Bean 56 // public EhCacheManager getCacheManager() {// 2、缓存配置 57 // EhCacheManager cacheManager = new EhCacheManager(); 58 // cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); 59 // return cacheManager; 60 // } 61 62 @Bean 63 public SessionIdGenerator getSessionIdGenerator() { // 3 64 return new JavaUuidSessionIdGenerator(); 65 } 66 //更换现在要使用的 SessionDAO 实现子类 67 @Bean 68 public SessionDAO getSessionDAO(SessionIdGenerator sessionIdGenerator) { // 4 69 RedisSessionDAO sessionDAO = new RedisSessionDAO(); // 使用Redis进行Session管理 70 sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); 71 sessionDAO.setSessionIdGenerator(sessionIdGenerator); 72 return sessionDAO; 73 } 74 75 @Bean 76 public RememberMeManager getRememberManager() { // 5 77 CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); 78 SimpleCookie cookie = new SimpleCookie("MLDNJAVA-RememberMe"); 79 cookie.setHttpOnly(true); 80 cookie.setMaxAge(3600); 81 rememberMeManager.setCookie(cookie); 82 return rememberMeManager; 83 } 84 85 @Bean 86 public QuartzSessionValidationScheduler getQuartzSessionValidationScheduler() { 87 QuartzSessionValidationScheduler sessionValidationScheduler = new QuartzSessionValidationScheduler(); 88 sessionValidationScheduler.setSessionValidationInterval(100000); 89 return sessionValidationScheduler; 90 } 91 92 @Bean 93 public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( 94 DefaultWebSecurityManager securityManager) { 95 AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); 96 aasa.setSecurityManager(securityManager); 97 return aasa; 98 } 99 100 @Bean 101 public DefaultWebSessionManager getSessionManager(SessionDAO sessionDAO, 102 QuartzSessionValidationScheduler sessionValidationScheduler) { // 6 103 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); 104 sessionManager.setGlobalSessionTimeout(1000000); 105 sessionManager.setDeleteInvalidSessions(true); 106 sessionManager.setSessionValidationScheduler(sessionValidationScheduler); 107 sessionManager.setSessionValidationSchedulerEnabled(true); 108 sessionManager.setSessionDAO(sessionDAO); 109 SimpleCookie sessionIdCookie = new SimpleCookie("mldn-session-id"); 110 sessionIdCookie.setHttpOnly(true); 111 sessionIdCookie.setMaxAge(-1); 112 sessionManager.setSessionIdCookie(sessionIdCookie); 113 sessionManager.setSessionIdCookieEnabled(true); 114 return sessionManager; 115 } 116 //更换使用的缓存组件 117 @Bean 118 public DefaultWebSecurityManager getSecurityManager(Realm memberRealm, RedisCacheManager cacheManager, 119 SessionManager sessionManager, RememberMeManager rememberMeManager) {// 7 120 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); 121 securityManager.setRealm(memberRealm); 122 securityManager.setCacheManager(cacheManager); 123 securityManager.setSessionManager(sessionManager); 124 securityManager.setRememberMeManager(rememberMeManager); 125 return securityManager; 126 } 127 128 public FormAuthenticationFilter getLoginFilter() { // 在ShiroFilterFactoryBean中使用 129 FormAuthenticationFilter filter = new FormAuthenticationFilter(); 130 filter.setUsernameParam("mid"); 131 filter.setPasswordParam("password"); 132 filter.setRememberMeParam("rememberMe"); 133 filter.setLoginUrl("/loginPage"); // 登录提交页面 134 filter.setFailureKeyAttribute("error"); 135 return filter; 136 } 137 138 public LogoutFilter getLogoutFilter() { // 在ShiroFilterFactoryBean中使用 139 LogoutFilter logoutFilter = new LogoutFilter(); 140 logoutFilter.setRedirectUrl("/"); // 首页路径,登录注销后回到的页面 141 return logoutFilter; 142 } 143 144 @Bean 145 public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { 146 ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); 147 // 必须设置 SecurityManager 148 shiroFilterFactoryBean.setSecurityManager(securityManager); 149 shiroFilterFactoryBean.setLoginUrl("/loginPage"); // 设置登录页路径 150 shiroFilterFactoryBean.setSuccessUrl("/pages/hello"); // 设置跳转成功页 151 shiroFilterFactoryBean.setUnauthorizedUrl("/pages/unauthUrl"); // 授权错误页 152 Map<String, Filter> filters = new HashMap<String, Filter>(); 153 filters.put("authc", this.getLoginFilter()); 154 filters.put("logout", this.getLogoutFilter()); 155 shiroFilterFactoryBean.setFilters(filters); 156 Map<String, String> filterChainDefinitionMap = new HashMap<String, String>(); 157 filterChainDefinitionMap.put("/logout", "logout"); 158 filterChainDefinitionMap.put("/loginPage", "authc"); // 定义内置登录处理 159 filterChainDefinitionMap.put("/pages/back/**", "authc"); 160 filterChainDefinitionMap.put("/*", "anon"); 161 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 162 return shiroFilterFactoryBean; 163 } 164 }
5、 【microboot-shiro-web】修改 application.yml 配置文件进行 Redis 配置:
spring:
redis:
host: x.x.x.x
port: 6379
password: studyjava
timeout: 1000
database: 0
pool:
max-active: 10
max-idle: 8
min-idle: 2
max-wait: 100
server:
port: 8080
6、 【microboot-shiro-web】建立一个 RedisTemplate 的配置程序类。
· 定义 Redis 序列化管理器:
package cn.study.microboot.util; import org.springframework.core.convert.converter.Converter; import org.springframework.core.serializer.support.DeserializingConverter; import org.springframework.core.serializer.support.SerializingConverter; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationException; public class RedisObjectSerializer implements RedisSerializer<Object> { private Converter<Object, byte[]> serializer = new SerializingConverter(); private Converter<byte[], Object> deserializer = new DeserializingConverter(); private static final byte[] EMPTY_ARRAY = new byte[0]; @Override public byte[] serialize(Object object) throws SerializationException { if (object == null) { return EMPTY_ARRAY; } try { return serializer.convert(object); } catch (Exception ex) { return EMPTY_ARRAY; } } @Override public Object deserialize(byte[] bytes) throws SerializationException { if (this.isEmpty(bytes)) { return null; } try { return deserializer.convert(bytes); } catch (Exception ex) { throw new SerializationException("序列化对象出错!", ex); } } private boolean isEmpty(byte[] data) { return (data == null || data.length == 0); } }
· 实现 RedisTemplate 配置程序类:
package cn.study.microboot.config; import cn.study.microboot.util.RedisObjectSerializer; import javax.annotation.Resource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Resource private JedisConnectionFactory jedisConnectionFactory; @Bean({"shiroRedis"}) public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(this.jedisConnectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new RedisObjectSerializer()); return template; } }
此时就使用了 Redis 实现了缓存处理,这样将适合于分布式集群开发。
2.5、thymeleaf 整合 Shiro 标签
在使用 JSP 的时候可以直接在 JSP 页面之中使用 shiro 标签来判断用户是否登录或者来进行授权检测,但是在 SpringBoot 里面 所使用的页面技术为 thymeleaf,那么如果要想在这样的模版页面之中实现 Shiro 控制,就必须去引入新的依赖包,同时做出一些新 的配置
1、 【microboot-shiro-web】修改 pom.xml 配置文件,追加 thymeleaf 与 shiro 的整合依赖:
<dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>1.2.1</version> </dependency>
2、 【microboot-shiro-web】随后需要修改一下 Shiro 配置类,在这个配置类之中需要启用 Shiro 页面支持:
package cn.study.microboot.config; import java.util.HashMap; import java.util.Map; import javax.servlet.Filter; import org.apache.shiro.mgt.RememberMeManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.apache.shiro.session.mgt.eis.SessionIdGenerator; import org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler; 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.filter.authc.FormAuthenticationFilter; import org.apache.shiro.web.filter.authc.LogoutFilter; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import at.pollux.thymeleaf.shiro.dialect.ShiroDialect; import cn.study.microboot.cache.RedisCacheManager; import cn.study.microboot.realm.CustomerCredentialsMatcher; import cn.study.microboot.realm.MemberRealm; import cn.study.microboot.session.RedisSessionDAO; @Configuration public class ShiroConfig { @Bean public ShiroDialect getShiroDialect() { // 必须配置此操作才可以使用thymeleaf-extras-shiro开发包 return new ShiroDialect() ; } @Bean public MemberRealm getRealm() {// 1、获取配置的Realm,之所以没使用注解配置,是因为此处需要考虑到加密处理 MemberRealm realm = new MemberRealm(); realm.setCredentialsMatcher(new CustomerCredentialsMatcher()); return realm; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator daap = new DefaultAdvisorAutoProxyCreator(); daap.setProxyTargetClass(true); return daap; } // @Bean // public EhCacheManager getCacheManager() {// 2、缓存配置 // EhCacheManager cacheManager = new EhCacheManager(); // cacheManager.setCacheManagerConfigFile("classpath:ehcache.xml"); // return cacheManager; // } @Bean public SessionIdGenerator getSessionIdGenerator() { // 3 return new JavaUuidSessionIdGenerator(); } @Bean public SessionDAO getSessionDAO(SessionIdGenerator sessionIdGenerator) { // 4 RedisSessionDAO sessionDAO = new RedisSessionDAO(); // 使用Redis进行Session管理 sessionDAO.setActiveSessionsCacheName("shiro-activeSessionCache"); sessionDAO.setSessionIdGenerator(sessionIdGenerator); return sessionDAO; } @Bean public RememberMeManager getRememberManager() { // 5 CookieRememberMeManager rememberMeManager = new CookieRememberMeManager(); SimpleCookie cookie = new SimpleCookie("studyJAVA-RememberMe"); cookie.setHttpOnly(true); cookie.setMaxAge(3600); rememberMeManager.setCookie(cookie); return rememberMeManager; } @Bean public QuartzSessionValidationScheduler getQuartzSessionValidationScheduler() { QuartzSessionValidationScheduler sessionValidationScheduler = new QuartzSessionValidationScheduler(); sessionValidationScheduler.setSessionValidationInterval(100000); return sessionValidationScheduler; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor aasa = new AuthorizationAttributeSourceAdvisor(); aasa.setSecurityManager(securityManager); return aasa; } @Bean public DefaultWebSessionManager getSessionManager(SessionDAO sessionDAO, QuartzSessionValidationScheduler sessionValidationScheduler) { // 6 DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(1000000); sessionManager.setDeleteInvalidSessions(true); sessionManager.setSessionValidationScheduler(sessionValidationScheduler); sessionManager.setSessionValidationSchedulerEnabled(true); sessionManager.setSessionDAO(sessionDAO); SimpleCookie sessionIdCookie = new SimpleCookie("study-session-id"); sessionIdCookie.setHttpOnly(true); sessionIdCookie.setMaxAge(-1); sessionManager.setSessionIdCookie(sessionIdCookie); sessionManager.setSessionIdCookieEnabled(true); return sessionManager; } @Bean public DefaultWebSecurityManager getSecurityManager(Realm memberRealm, RedisCacheManager cacheManager, SessionManager sessionManager, RememberMeManager rememberMeManager) {// 7 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(memberRealm); securityManager.setCacheManager(cacheManager); securityManager.setSessionManager(sessionManager); securityManager.setRememberMeManager(rememberMeManager); return securityManager; } public FormAuthenticationFilter getLoginFilter() { // 在ShiroFilterFactoryBean中使用 FormAuthenticationFilter filter = new FormAuthenticationFilter(); filter.setUsernameParam("mid"); filter.setPasswordParam("password"); filter.setRememberMeParam("rememberMe"); filter.setLoginUrl("/loginPage"); // 登录提交页面 filter.setFailureKeyAttribute("error"); return filter; } public LogoutFilter getLogoutFilter() { // 在ShiroFilterFactoryBean中使用 LogoutFilter logoutFilter = new LogoutFilter(); logoutFilter.setRedirectUrl("/"); // 首页路径,登录注销后回到的页面 return logoutFilter; } @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/loginPage"); // 设置登录页路径 shiroFilterFactoryBean.setSuccessUrl("/pages/hello"); // 设置跳转成功页 shiroFilterFactoryBean.setUnauthorizedUrl("/pages/unauthUrl"); // 授权错误页 Map<String, Filter> filters = new HashMap<String, Filter>(); filters.put("authc", this.getLoginFilter()); filters.put("logout", this.getLogoutFilter()); shiroFilterFactoryBean.setFilters(filters); Map<String, String> filterChainDefinitionMap = new HashMap<String, String>(); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/loginPage", "authc"); // 定义内置登录处理 filterChainDefinitionMap.put("/pages/back/**", "authc"); filterChainDefinitionMap.put("/*", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } }
3、 【microboot-shiro-web】建立一个新的页面:dept_show.html 页面;
· 修改 DeptController 程序类进行一个跳转的配置:
package cn.study.microboot.controller; import org.apache.shiro.authz.annotation.RequiresAuthentication; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class DeptController { @RequiresAuthentication @RequestMapping("/pages/back/dept/get") @ResponseBody public String get() { return "部门信息" ; } @RequestMapping("/pages/back/dept/show") public String show() { return "dept_show" ; } }
· 建立 dept_show.html 页面,而后在页面之中需要编写以下代码:
<!DOCTYPE HTML> <html xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <title>SpringBoot模版渲染</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> </head> <body> <h1>显示部门信息的内容</h1> <h2>欢迎:<shiro:principal/></h2> </body> </html>
4、 【microboot-shiro-web】修改 dept_show.html 页面进行认证与授权的处理操作。
<!DOCTYPE HTML> <html xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"> <head> <title>SpringBoot模版渲染</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/> </head> <body> <h1>显示部门信息的内容</h1> <h2>欢迎:<shiro:principal/></h2> <p><a shiro:hasRole="emp">雇员管理</a></p> <p><a shiro:hasRole="dept">部门管理</a></p> <p><a shiro:hasPermission="emp:add">雇员增加</a></p> <p><a shiro:hasPermission="dept:edit">部门修改</a></p> <p shiro:notAuthenticated="">您还未登录,请先登录!</p> <p shiro:authenticated="">欢迎光临!</p> </body> </html>
如果在以后进行 Shiro 与 SpringBoot 整合的时候一定要考虑使用如上的标签进行整体处理。
3、总结
SpringBoot 总结:
· 优点:
|- Rest 支持度高,整体的开发难度相对于 SSM、SSH 整合还是挺简单的;
|- 与各个服务的单一集成很方便,但是如果要进行多集成就非常麻烦了,需要编写各种配置类;
|- thymeleaf 作为一款优秀的页面模版工具,所带来的功能的确强悍,页面开发更简单;
|- 与它想整合的开发框架整合方便;
|- 方便使用 jar 包进行项目部署与发布;
· 缺点:
|- thymeleaf 页面开发要求较高,因为语法严格;
|- 太简单了,让人不适应。
SpringBoot 中的 Rest 就是迈向 SpringCloud 的第一步。