spring-boot-2.0.3不一样系列之shiro - 搭建篇
前言
上一篇:,讲了如何实现国际化,实际上我工作用的模版引擎是freemaker,而不是thymeleaf,不过原理都是相通的。
接着上一篇,这一篇我来讲讲spring-boot如何整合工作中用到的一个非常重要的功能:安全,而本文的主角就是一个安全框架:shiro。
apache shiro是java的一个安全框架。目前,使用apache shiro的人也越来越多,因为它相当简单,对比spring security,可能没有spring security的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的shiro就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
摘自开涛兄的《跟我学shiro》
本文旨在整合spring-boot与shiro,实现简单的认证功能,shiro的更多使用细节大家可以去阅读《更我学shiro》或者看官方文档。
本文项目地址:
spring-boot整合shiro
集成mybatis
shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供,然后通过相应的接口注入给shiro;既然用户、权限这些信息需要我们自己设计、维护,那么可想而知需要进行数据库表的设计了(具体表结构看后文),既然涉及到数据库的操作,那么我们就先整合mybatis,实现数据库的操作。
pom.xml:
<?xml version="1.0" encoding="utf-8"?> <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <groupid>com.lee</groupid> <artifactid>spring-boot-shiro</artifactid> <version>1.0-snapshot</version> <properties> <java.version>1.8</java.version> </properties> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.0.3.release</version> </parent> <dependencies> <!-- mybatis相关 --> <dependency> <groupid>com.github.pagehelper</groupid> <artifactid>pagehelper-spring-boot-starter</artifactid> <version>1.2.5</version> </dependency> <dependency> <groupid>com.alibaba</groupid> <artifactid>druid-spring-boot-starter</artifactid> <version>1.1.10</version> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <!-- test --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project>
配置文件application.yml:
spring: #连接池配置 datasource: type: com.alibaba.druid.pool.druiddatasource druid: driver-class-name: com.mysql.jdbc.driver url: jdbc:mysql://localhost:3306/spring-boot?usessl=false&useunicode=true&characterencoding=utf-8 username: root password: 123456 initial-size: 1 #连接池初始大小 max-active: 20 #连接池中最大的活跃连接数 min-idle: 1 #连接池中最小的活跃连接数 max-wait: 60000 #配置获取连接等待超时的时间 pool-prepared-statements: true #打开pscache,并且指定每个连接上pscache的大小 max-pool-prepared-statement-per-connection-size: 20 validation-query: select 1 from dual validation-query-timeout: 30000 test-on-borrow: false #是否在获得连接后检测其可用性 test-on-return: false #是否在连接放回连接池后检测其可用性 test-while-idle: true #是否在连接空闲一段时间后检测其可用性 #mybatis配置 mybatis: type-aliases-package: com.lee.shiro.entity #config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml # pagehelper配置 pagehelper: helperdialect: mysql #分页合理化,pagenum<=0则查询第一页的记录;pagenum大于总页数,则查询最后一页的记录 reasonable: true supportmethodsarguments: true params: count=countsql
在数据库spring-boot中新建表tbl_user:
drop table if exists `tbl_user`; create table `tbl_user` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `username` varchar(50) not null comment '名称', `password` char(32) not null comment '密码', `salt` char(32) not null comment '盐,用于加密', `state` tinyint(2) not null default '1' comment '状态, 1:可用, 0:不可用', `description` varchar(50) default '' comment '描述', primary key (`id`) ) engine=innodb auto_increment=6 default charset=utf8mb4 comment='用户表'; -- ---------------------------- -- records of tbl_user -- ---------------------------- insert into `tbl_user` values ('1', 'admin', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', '1', 'bing,作者自己'); insert into `tbl_user` values ('2', 'brucelee', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '龙的传人'); insert into `tbl_user` values ('3', 'zhangsan', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '张三'); insert into `tbl_user` values ('4', 'lisi', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '李四'); insert into `tbl_user` values ('5', 'jiraya', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '自来也');
mapper接口:usermapper.java
package com.lee.shiro.mapper; import com.lee.shiro.entity.user; import org.apache.ibatis.annotations.mapper; import org.apache.ibatis.annotations.param; import java.util.list; @mapper public interface usermapper { /** * 根据用户名获取用户 * @param username * @return */ user finduserbyusername(@param("username") string username); }
usermapper.xml:
<?xml version="1.0" encoding="utf-8" ?> <!doctype mapper public "-//mybatis.org//dtd mapper 3.0//en" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.lee.shiro.mapper.usermapper"> <select id="finduserbyusername" resulttype="user"> select id,username,password,salt,state,description from tbl_user where username=#{username} </select> </mapper>
service接口:iuserservice.java
package com.lee.shiro.service; import com.lee.shiro.entity.user; public interface iuserservice { /** * 根据用户名获取用户 * @param username * @return */ user finduserbyusername(string username); }
service实现:userserviceimpl.java
package com.lee.shiro.service.impl; import com.lee.shiro.entity.user; import com.lee.shiro.mapper.usermapper; import com.lee.shiro.service.iuserservice; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.service; import java.util.list; @service public class userserviceimpl implements iuserservice { @autowired private usermapper usermapper; @override public user finduserbyusername(string username) { user user = usermapper.finduserbyusername(username); return user; } }
启动类:shiroapplication.java
package com.lee.shiro; import org.springframework.boot.springapplication; import org.springframework.boot.autoconfigure.springbootapplication; @springbootapplication public class shiroapplication { public static void main(string[] args) { springapplication.run(shiroapplication.class, args); } }
测试类:mybatistest.java
package com.lee.shiro.test; import com.lee.shiro.shiroapplication; import com.lee.shiro.entity.user; import com.lee.shiro.service.iuserservice; import org.junit.assert; import org.junit.test; import org.junit.runner.runwith; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.test.context.springboottest; import org.springframework.test.context.junit4.springrunner; @runwith(springrunner.class) @springboottest(classes = shiroapplication.class) public class mybatistest { @autowired private iuserservice userservice; @test public void testfinduserbyusername() { user user = userservice.finduserbyusername("brucelee"); assert.assertequals(user.getdescription(), "龙的传人"); } }
测试用例顺利通过,则表示mybatis集成成功
开启logback日志
其实上面的pom配置已经引入了日志依赖,如图:
但是你会发现,spring-boot-starter-logging引入了3种类型的日志,你用其中任何一种都能正常打印日志;但是我们需要用3种吗?根本用不到,我们只要用一种即可,至于选用那种,全凭大家自己的喜欢;我了,比较喜欢logback(接触的项目中用的比较多,说白了就是这3种中最熟悉的把);我们来改下pom.xml,重新配置日志依赖:
<?xml version="1.0" encoding="utf-8"?> <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <groupid>com.lee</groupid> <artifactid>spring-boot-shiro</artifactid> <version>1.0-snapshot</version> <properties> <java.version>1.8</java.version> </properties> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.0.3.release</version> </parent> <dependencies> <!-- mybatis相关 --> <dependency> <groupid>com.github.pagehelper</groupid> <artifactid>pagehelper-spring-boot-starter</artifactid> <version>1.2.5</version> </dependency> <dependency> <groupid>com.alibaba</groupid> <artifactid>druid-spring-boot-starter</artifactid> <version>1.1.10</version> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <!-- 日志 --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-logging</artifactid> <exclusions> <!-- 剔除spring-boot-starter-logging中的全部依赖 --> <exclusion> <groupid>*</groupid> <artifactid>*</artifactid> </exclusion> </exclusions> <scope>test</scope> <!-- package或install的时候,spring-boot-starter-logging.jar也不会打进去 --> </dependency> <dependency> <groupid>ch.qos.logback</groupid> <artifactid>logback-classic</artifactid> </dependency> <!-- test --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project>
logback.xml:
<?xml version="1.0" encoding="utf-8"?> <configuration debug="false"> <!--定义日志文件的存储地址 勿在 logback 的配置中使用相对路径 --> <property name="log_home" value="/log" /> <!-- 控制台输出 --> <appender name="stdout" class="ch.qos.logback.core.consoleappender"> <encoder class="ch.qos.logback.classic.encoder.patternlayoutencoder"> <pattern>%d{yyyy-mm-dd hh:mm:ss} |%logger| |%level|%msg%n</pattern> </encoder> </appender> <!-- 按照每天生成日志文件 --> <appender name="file" class="ch.qos.logback.core.rolling.rollingfileappender"> <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy"> <!--日志文件输出的文件名 --> <filenamepattern>${log_home}/spring-boot-shiro.log.%d{yyyy-mm-dd}.log</filenamepattern> <!--日志文件保留天数 --> <maxhistory>30</maxhistory> </rollingpolicy> <encoder class="ch.qos.logback.classic.encoder.patternlayoutencoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 --> <pattern>%d{yyyy-mm-dd hh:mm:ss} |%logger| |%level|%msg%n</pattern> </encoder> <!--日志文件最大的大小 --> <triggeringpolicy class="ch.qos.logback.core.rolling.sizebasedtriggeringpolicy"> <maxfilesize>10mb</maxfilesize> </triggeringpolicy> </appender> <!-- 日志输出级别 --> <root level="info"> <appender-ref ref="stdout" /> <appender-ref ref="file" /> </root> </configuration>
开启web功能
在pom.xml中加入web依赖和thymeleaf依赖:
<?xml version="1.0" encoding="utf-8"?> <project xmlns="http://maven.apache.org/pom/4.0.0" xmlns:xsi="http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation="http://maven.apache.org/pom/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelversion>4.0.0</modelversion> <groupid>com.lee</groupid> <artifactid>spring-boot-shiro</artifactid> <version>1.0-snapshot</version> <properties> <java.version>1.8</java.version> </properties> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>2.0.3.release</version> </parent> <dependencies> <!-- mybatis相关 --> <dependency> <groupid>com.github.pagehelper</groupid> <artifactid>pagehelper-spring-boot-starter</artifactid> <version>1.2.5</version> </dependency> <dependency> <groupid>com.alibaba</groupid> <artifactid>druid-spring-boot-starter</artifactid> <version>1.1.10</version> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <!-- 日志 --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-logging</artifactid> <exclusions> <!-- 剔除spring-boot-starter-logging中的全部依赖 --> <exclusion> <groupid>*</groupid> <artifactid>*</artifactid> </exclusion> </exclusions> <scope>test</scope> <!-- package或install的时候,spring-boot-starter-logging.jar也不会打进去 --> </dependency> <dependency> <groupid>ch.qos.logback</groupid> <artifactid>logback-classic</artifactid> </dependency> <!-- web --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency> <!-- test --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> </project>
application.yml中加入端口配置:
server: port: 8881 spring: #连接池配置 datasource: type: com.alibaba.druid.pool.druiddatasource druid: driver-class-name: com.mysql.jdbc.driver url: jdbc:mysql://localhost:3306/spring-boot?usessl=false&useunicode=true&characterencoding=utf-8 username: root password: 123456 initial-size: 1 #连接池初始大小 max-active: 20 #连接池中最大的活跃连接数 min-idle: 1 #连接池中最小的活跃连接数 max-wait: 60000 #配置获取连接等待超时的时间 pool-prepared-statements: true #打开pscache,并且指定每个连接上pscache的大小 max-pool-prepared-statement-per-connection-size: 20 validation-query: select 1 from dual validation-query-timeout: 30000 test-on-borrow: false #是否在获得连接后检测其可用性 test-on-return: false #是否在连接放回连接池后检测其可用性 test-while-idle: true #是否在连接空闲一段时间后检测其可用性 #mybatis配置 mybatis: type-aliases-package: com.lee.shiro.entity #config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mybatis/mapper/*.xml # pagehelper配置 pagehelper: helperdialect: mysql #分页合理化,pagenum<=0则查询第一页的记录;pagenum大于总页数,则查询最后一页的记录 reasonable: true supportmethodsarguments: true params: count=countsql
加入controller,处理web请求,具体代码参考:
用post测试下,出现下图,表示web开启成功
配置druid监控后台
可配可不配,但是建议配置上,它能提供很多监控信息,对排查问题非常有帮助,配置好后,界面如下
提供的内容还是非常多的,更多的druid配置大家可以查看
druid配置只需要在application.yml中加入druid配置,同时在config目录下加上druidconfig.java配置文件即可,具体内容可参考:
集成shiro,并用redis实现shiro缓存
集成shiro非常简单,我们只需要将用户、权限信息传给shiro即可。表结构信息:
drop table if exists `tbl_user`; create table `tbl_user` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `username` varchar(50) not null comment '名称', `password` char(32) not null comment '密码', `salt` char(32) not null comment '盐,用于加密', `state` tinyint(2) not null default '1' comment '状态, 1:可用, 0:不可用', `description` varchar(50) default '' comment '描述', primary key (`id`) ) engine=innodb default charset=utf8mb4 comment='用户表'; -- ---------------------------- -- records of tbl_user -- ---------------------------- insert into `tbl_user` values ('1', 'admin', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', '1', 'bing,作者自己'); insert into `tbl_user` values ('2', 'brucelee', '5d5c735291a524c80c53ff669d2cde1b', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '龙的传人'); insert into `tbl_user` values ('3', 'zhangsan', 'b8432e3a2a5adc908bd4ff22ba1f2d65', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '张三'); insert into `tbl_user` values ('4', 'lisi', '1fdda90367c23a1f1230eb202104270a', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '李四'); insert into `tbl_user` values ('5', 'jiraya', 'e7c5afb5e2fe7da78641721f2c5aad82', '78d92ba9477b3661bc8be4bd2e8dd8c0', '1', '自来也'); -- ---------------------------- -- table structure for `tbl_user_role` -- ---------------------------- drop table if exists `tbl_user_role`; create table `tbl_user_role` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `user_id` int(10) unsigned not null comment '用户id', `role_id` int(10) unsigned not null comment '角色id', primary key (`id`) ) engine=innodb default charset=utf8mb4 comment='用户角色表'; -- ---------------------------- -- records of tbl_user_role -- ---------------------------- insert into `tbl_user_role` values ('1', '1', '1'); insert into `tbl_user_role` values ('2', '2', '4'); -- ---------------------------- -- table structure for `tbl_permission` -- ---------------------------- drop table if exists `tbl_permission`; create table `tbl_permission` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `name` varchar(50) not null comment '名称', `permission` varchar(50) not null comment '权限', `url` varchar(50) not null comment 'url', `description` varchar(50) default '' comment '描述', primary key (`id`) ) engine=innodb default charset=utf8mb4 comment='权限表'; -- ---------------------------- -- records of tbl_permission -- ---------------------------- insert into `tbl_permission` values ('1', '用户列表', 'user:view', 'user/userlist', '用户列表'); insert into `tbl_permission` values ('2', '用户添加', 'user:add', 'user/useradd', '用户添加'); insert into `tbl_permission` values ('3', '用户删除', 'user:del', 'user/userdel', '用户删除'); -- ---------------------------- -- table structure for `tbl_role` -- ---------------------------- drop table if exists `tbl_role`; create table `tbl_role` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `name` varchar(50) not null comment '名称', `description` varchar(50) default '' comment '描述', primary key (`id`) ) engine=innodb default charset=utf8mb4 comment='角色表'; -- ---------------------------- -- records of tbl_role -- ---------------------------- insert into `tbl_role` values ('1', '超级管理员', '拥有全部权限'); insert into `tbl_role` values ('2', '角色管理员', '拥有全部查看权限,以及角色的增删改权限'); insert into `tbl_role` values ('3', '权限管理员', '拥有全部查看权限,以及权限的增删改权限'); insert into `tbl_role` values ('4', '用户管理员', '拥有全部查看权限,以及用户的增删改权限'); insert into `tbl_role` values ('5', '审核管理员', '拥有全部查看权限,以及审核的权限'); -- ---------------------------- -- table structure for `tbl_role_permission` -- ---------------------------- drop table if exists `tbl_role_permission`; create table `tbl_role_permission` ( `id` int(10) unsigned not null auto_increment comment '自增主键', `role_id` int(10) unsigned not null comment '角色id', `permission_id` int(10) unsigned not null comment '权限id', primary key (`id`) ) engine=innodb default charset=utf8mb4 comment='角色权限表'; -- ---------------------------- -- records of tbl_role_permission -- ---------------------------- insert into `tbl_role_permission` values ('1', '1', '1'); insert into `tbl_role_permission` values ('2', '1', '2'); insert into `tbl_role_permission` values ('3', '1', '3'); insert into `tbl_role_permission` values ('4', '4', '1'); insert into `tbl_role_permission` values ('5', '4', '2'); insert into `tbl_role_permission` values ('6', '4', '3');
实现role、permission的mapper(user的在之前已经实现了),然后将用户信息、权限信息注入到shiro的realm中即可,shiroconfig.java:
package com.lee.shiro.config; import com.lee.shiro.entity.role; import com.lee.shiro.entity.user; import com.lee.shiro.mapper.permissionmapper; import com.lee.shiro.mapper.rolemapper; import com.lee.shiro.service.iuserservice; import com.lee.shiro.util.bytesourceutils; import org.apache.shiro.authc.authenticationexception; import org.apache.shiro.authc.authenticationinfo; import org.apache.shiro.authc.authenticationtoken; import org.apache.shiro.authc.simpleauthenticationinfo; import org.apache.shiro.authc.credential.hashedcredentialsmatcher; import org.apache.shiro.authz.authorizationinfo; import org.apache.shiro.authz.simpleauthorizationinfo; import org.apache.shiro.cache.cachemanager; import org.apache.shiro.mgt.securitymanager; import org.apache.shiro.realm.authorizingrealm; import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor; import org.apache.shiro.spring.web.shirofilterfactorybean; import org.apache.shiro.subject.principalcollection; import org.apache.shiro.web.mgt.cookieremembermemanager; import org.apache.shiro.web.mgt.defaultwebsecuritymanager; import org.apache.shiro.web.servlet.simplecookie; import org.slf4j.logger; import org.slf4j.loggerfactory; import org.springframework.aop.framework.autoproxy.defaultadvisorautoproxycreator; import org.springframework.beans.factory.annotation.autowired; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.handler.simplemappingexceptionresolver; import java.util.linkedhashmap; import java.util.list; import java.util.map; import java.util.properties; @configuration public class shiroconfig { private static final logger logger = loggerfactory.getlogger(shiroconfig.class); @autowired private iuserservice userservice; @autowired private rolemapper rolemapper; @autowired private permissionmapper permissionmapper; @bean public shirofilterfactorybean shirofilterfactorybean(securitymanager securitymanager) { shirofilterfactorybean shirofilterfactorybean = new shirofilterfactorybean(); shirofilterfactorybean.setsecuritymanager(securitymanager); map<string, string> filterchaindefinitionmap = new linkedhashmap<>(); filterchaindefinitionmap.put("/logout", "logout"); filterchaindefinitionmap.put("/favicon.ico", "anon"); filterchaindefinitionmap.put("/druid/**", "anon"); // druid登录交给druid自己 filterchaindefinitionmap.put("/**", "authc"); //authc表示需要验证身份才能访问,还有一些比如anon表示不需要验证身份就能访问等。 shirofilterfactorybean.setloginurl("/login"); shirofilterfactorybean.setsuccessurl("/index"); shirofilterfactorybean.setfilterchaindefinitionmap(filterchaindefinitionmap); return shirofilterfactorybean; } @bean public securitymanager securitymanager(authorizingrealm myshirorealm, cachemanager shirorediscachemanager) { defaultwebsecuritymanager securitymanager = new defaultwebsecuritymanager(); securitymanager.setcachemanager(shirorediscachemanager); securitymanager.setremembermemanager(cookieremembermemanager()); securitymanager.setrealm(myshirorealm); return securitymanager; } @bean public authorizingrealm myshirorealm(hashedcredentialsmatcher hashedcredentialsmatcher) { authorizingrealm myshirorealm = new authorizingrealm() { @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { logger.info("认证 --> myshirorealm.dogetauthenticationinfo()"); //获取用户的输入的账号. string username = (string)token.getprincipal(); logger.info("界面输入的用户名:{}", username); //通过username从数据库中查找 user对象, user user = userservice.finduserbyusername(username); if(user == null){ //没有返回登录用户名对应的simpleauthenticationinfo对象时,就会在logincontroller中抛出unknownaccountexception异常 return null; } simpleauthenticationinfo authenticationinfo = new simpleauthenticationinfo( user, //用户名 user.getpassword(), //密码 bytesourceutils.bytes(user.getcredentialssalt()),//salt=username+salt getname() //realm name ); return authenticationinfo; } @override protected authorizationinfo dogetauthorizationinfo(principalcollection principal) { logger.info("权限配置 --> myshirorealm.dogetauthorizationinfo()"); simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo(); user user = (user)principal.getprimaryprincipal(); list<role> roles = rolemapper.findrolebyusername(user.getusername()); logger.info("用户:{}, 角色有{}个", user.getusername(), roles.size()); roles.stream().foreach( role -> { authorizationinfo.addrole(role.getname()); permissionmapper.findpermissionbyroleid(role.getid()).stream().foreach( permission -> { authorizationinfo.addstringpermission(permission.getpermission()); } ); } ); return authorizationinfo; } }; myshirorealm.setcredentialsmatcher(hashedcredentialsmatcher); //设置加密规则 myshirorealm.setcachingenabled(true); myshirorealm.setauthorizationcachingenabled(true); myshirorealm.setauthenticationcachingenabled(true); return myshirorealm; } // 需要与存储密码时的加密规则一致 @bean public hashedcredentialsmatcher hashedcredentialsmatcher() { hashedcredentialsmatcher hashedcredentialsmatcher = new hashedcredentialsmatcher(); hashedcredentialsmatcher.sethashalgorithmname("md5");//散列算法:这里使用md5算法; hashedcredentialsmatcher.sethashiterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedcredentialsmatcher; } /** * defaultadvisorautoproxycreator,spring的一个bean,由advisor决定对哪些类的方法进行aop代理< * @return */ @bean public defaultadvisorautoproxycreator defaultadvisorautoproxycreator() { defaultadvisorautoproxycreator proxycreator = new defaultadvisorautoproxycreator(); proxycreator.setproxytargetclass(true); return proxycreator; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param securitymanager * @return */ @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(securitymanager securitymanager){ authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager); return authorizationattributesourceadvisor; } @bean public simplemappingexceptionresolver resolver() { simplemappingexceptionresolver exceptionresolver = new simplemappingexceptionresolver(); properties properties = new properties(); properties.setproperty("unauthorizedexception", "/403"); exceptionresolver.setexceptionmappings(properties); return exceptionresolver; } //cookie对象; @bean public simplecookie remembermecookie() { logger.info("shiroconfiguration.remembermecookie()"); //这个参数是cookie的名称,对应前端的checkbox的name = rememberme simplecookie simplecookie = new simplecookie("rememberme"); //<!-- 记住我cookie生效时间 ,单位秒;--> simplecookie.setmaxage(60); return simplecookie; } //cookie管理对象; @bean public cookieremembermemanager cookieremembermemanager() { logger.info("shiroconfiguration.remembermemanager()"); cookieremembermemanager manager = new cookieremembermemanager(); manager.setcookie(remembermecookie()); return manager; } }
shiro的缓存也是提供的接口,我们实现该接口即可接入我们自己的缓存实现,至于具体的缓存实现是redis、memcache还是其他的,shiro并不关心;而本文用redis实现shiro的缓存。采用spring的redistemplate来操作redis,具体的实现,如下
shirorediscachemanager:
package com.lee.shiro.config; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cacheexception; import org.apache.shiro.cache.cachemanager; import org.springframework.beans.factory.annotation.autowired; import org.springframework.stereotype.component; @component public class shirorediscachemanager implements cachemanager { @autowired private cache shirorediscache; @override public <k, v> cache<k, v> getcache(string s) throws cacheexception { return shirorediscache; } }
shirorediscache:
package com.lee.shiro.config; import org.apache.shiro.cache.cache; import org.apache.shiro.cache.cacheexception; import org.springframework.beans.factory.annotation.autowired; import org.springframework.beans.factory.annotation.value; import org.springframework.data.redis.core.redistemplate; import org.springframework.stereotype.component; import java.util.collection; import java.util.set; import java.util.concurrent.timeunit; @component public class shirorediscache<k,v> implements cache<k,v>{ @autowired private redistemplate<k,v> redistemplate; @value("${spring.redis.expiretime}") private long expiretime; @override public v get(k k) throws cacheexception { return redistemplate.opsforvalue().get(k); } @override public v put(k k, v v) throws cacheexception { redistemplate.opsforvalue().set(k,v,expiretime, timeunit.seconds); return null; } @override public v remove(k k) throws cacheexception { v v = redistemplate.opsforvalue().get(k); redistemplate.opsforvalue().getoperations().delete(k); return v; } @override public void clear() throws cacheexception { } @override public int size() { return 0; } @override public set<k> keys() { return null; } @override public collection<v> values() { return null; } }
更详细、完整的代码请参考,上文的缓存只是针对realm缓存,也就是权限相关的,至于其他缓存像session缓存,大家可以自行去实现。
效果展示
经过上述的步骤,工程已经搭建完毕我们来验证下效果
druid后台监控
如下图
在shiro配置中,我们放行了/druid/**,所以druid后台的地址都没有被拦截,druid相关的由druid自己控制,不受shiro的影响。
shiro权限控制
由spring-boot-shiro.sql、usercontroller.java可知,5个用户中只有admin和brucelee有/user/userlist、/user/useradd、/user/userdel的访问权限,而/user/finduserbyusername没做权限限制,那么5个用户都可以访问;但是登录是必须的(5个用户的密码都是123456);效果如下:
上图中展示了zhangsan用户和admin权限访问的情况,完全按照我们设想的剧本走的,剩下的用户大家可以自己去测试;另外还可以多设置一些权限来进行验证。
预祝大家搭建成功,如果有什么问题,可以@我,或者直接和我的代码进行比较,找出其中的问题。
疑问与解答
1、我不修改日志依赖,但是我只用其中的某种日志打印日志不就行了,不会冲突也能正常打日志,为什么要修改日志依赖?
说的没错,你不修改依赖也能正常工作,还不用书写更多的pom配置;但是你仔细去观察的话,你会发现你工程打包出来的时候,这些依赖的日志jar包全在包中,项目部署的时候,这些jar都会加载到内存中的,你没用到的日志jar也会加载到内存中,数量少、jar包小还能接受,一旦无用的jar包数量多、jar文件太大,那可想而知会浪费多少内存资源;内存资源不比磁盘,是比较稀有的。
强烈建议把无用的依赖剔除掉,既能节省资源、也能避免未知的一些错误。
2、日志依赖:为什么按文中的配置就能只依赖logback了
maven的依赖有两个原则:最短路径原则、最先声明原则;以我们的pom.xml为起点,那我们自定义的spring-boot-starter-logging依赖路径肯定最短了,那么maven就会选用我们自定义的spring-boot-starter-logging,所以就把spring-boot-starter-logging的依赖全部剔除了,而<scope>test<scope>,大家都懂的;至于最先声明原则,也就说在路径相同的情况下,谁在前声明就依赖谁。
3、遇到的一个坑,认证通过后,为什么授权回调没有被调用
首先要明白,认证与授权触发的时间点是不同的,登录触发认证,但是登录成功后不会立即触发授权的;授权是有权限校验的时候才触发的;大家请看下图
登录只是触发了认证、当有权限校验的时候才会授权(角色校验的时候也会),第一次权限校验请求数据库,数据会缓存到redis中,下次权限校验的时候就从缓存中获取,而不用再从数据库获取了。
另外shiro注解生效是配置两个bean的,defaultadvisorautoproxycreator和authorizationattributesourceadvisor,我在这个问题上卡了一段时间;只配置authorizationattributesourceadvisor没用,代理没打开,shiro注解的代理类就不会生成,注解配置了相当于没配置,这里需要大家注意。
参考
《跟我学shiro》
上一篇: PHP 采集心得技巧
推荐阅读
-
spring-boot-2.0.3不一样系列之番外篇 - 自定义session管理,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之源码篇 - 阶段总结
-
spring-boot-2.0.3不一样系列之番外篇 - springboot事件机制,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之源码篇 - SpringApplication的run方法(一)之SpringApplicationRunListener,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之shiro - 搭建篇
-
spring-boot-2.0.3不一样系列之源码篇 - run方法(三)之createApplicationContext,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之番外篇 - 自定义session管理,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之源码篇 - 阶段总结
-
spring-boot-2.0.3不一样系列之源码篇 - run方法(四)之prepareContext,绝对有值得你看的地方
-
spring-boot-2.0.3不一样系列之源码篇 - SpringApplication的run方法(一)之SpringApplicationRunListener,绝对有值得你看的地方