spring boot 2 + shiro 实现权限管理
shiro是一个功能强大且易于使用的java安全框架,主要功能有身份验证、授权、加密和会话管理。
看了网上一些文章,下面2篇文章写得不错。
springboot2.0 集成shiro权限管理
spring boot:整合shiro权限框架
自己动手敲了下代码,在第一篇文章上加入了第二篇文章的swagger测试,另外自己加入lombok简化实体类代码,一些地方代码也稍微修改了下,过程中也碰到一些问题,最终代码成功运行。
开发版本:
intellij idea 2019.2.2
jdk1.8
spring boot 2.1.11
mysql8.0
一、创建springboot项目,添加依赖包和配置application.yml
在idea中创建一个新的springboot项目
1、pom.xml引用的依赖包如下:
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.apache.shiro</groupid> <artifactid>shiro-spring</artifactid> <version>1.4.2</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-jpa</artifactid> </dependency> <dependency> <groupid>mysql</groupid> <artifactid>mysql-connector-java</artifactid> </dependency> <dependency> <groupid>org.projectlombok</groupid> <artifactid>lombok</artifactid> <version>1.18.10</version> <scope>provided</scope> </dependency> <dependency> <groupid>io.springfox</groupid> <artifactid>springfox-swagger2</artifactid> <version>2.9.2</version> </dependency> <dependency> <groupid>io.springfox</groupid> <artifactid>springfox-swagger-ui</artifactid> <version>2.9.2</version> </dependency>
2、application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.driver url: jdbc:mysql://localhost:3306/testdb?usessl=false&servertimezone=utc username: root password: 123456 jpa: hibernate: ddl-auto: update #指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建 naming: physical-strategy: org.hibernate.boot.model.naming.physicalnamingstrategystandardimpl #按字段名字建表 #implicit-strategy: org.hibernate.boot.model.naming.implicitnamingstrategylegacyjpaimpl #驼峰自动映射为下划线格式 show-sql: true # 默认false,在日志里显示执行的sql语句 database: mysql database-platform: org.hibernate.dialect.mysql5innodbdialect
二、创建实体类
创建user、role、permission三个实体类,根据规则会自动生成两个中间表,最终数据库有5个表。
另外添加一个model处理登录结果。
1、user
package com.example.shiro.entity; import lombok.getter; import lombok.setter; import org.springframework.format.annotation.datetimeformat; import javax.persistence.*; import java.time.localdate; import java.time.localdatetime; import java.util.list; @entity @getter @setter public class user { @id @generatedvalue(strategy = generationtype.auto) private long userid; @column(nullable = false, unique = true) private string username; //登录用户名 @column(nullable = false) private string name;//名称(昵称或者真实姓名,根据实际情况定义) @column(nullable = false) private string password; private string salt;//加密密码的盐 private byte state;//用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定. @manytomany(fetch= fetchtype.eager)//立即从数据库中进行加载数据; @jointable(name = "userrole", joincolumns = { @joincolumn(name = "userid") }, inversejoincolumns ={@joincolumn(name = "roleid") }) private list<role> rolelist;// 一个用户具有多个角色 @datetimeformat(pattern = "yyyy-mm-dd hh:mm") private localdatetime createtime;//创建时间 @datetimeformat(pattern = "yyyy-mm-dd") private localdate expireddate;//过期日期 private string email; /**密码盐. 重新对盐重新进行了定义,用户名+salt,这样就不容易被破解 */ public string getcredentialssalt(){ return this.username+this.salt; } }
说明:
实体使用了jpa的@onetomany 和lombok的@data,在运行过程中调用关联表数据时会显示异常 java.lang.*error。
因为使用@onetomany默认配置,所以加载方式为lazy。在主表查询时关联表未加载,而主表使用@data后会实现带关联表属性的hashcode和equals等方法。
所以这里不使用@data注解,而是用@getter,@setter注解。
2、role
package com.example.shiro.entity; import lombok.getter; import lombok.setter; import javax.persistence.*; import java.util.list; @entity @getter @setter public class role { @id @generatedvalue(strategy = generationtype.auto) private long roleid; // 编号 @column(nullable = false, unique = true) private string role; // 角色标识程序中判断使用,如"admin",这个是唯一的: private string description; // 角色描述,ui界面显示使用 private boolean available = boolean.true; // 是否可用,如果不可用将不会添加给用户 //角色 -- 权限关系:多对多关系; @manytomany(fetch= fetchtype.eager) @jointable(name="rolepermission",joincolumns={@joincolumn(name="roleid")},inversejoincolumns={@joincolumn(name="permissionid")}) private list<permission> permissions; // 用户 - 角色关系定义; @manytomany @jointable(name="userrole",joincolumns={@joincolumn(name="roleid")},inversejoincolumns={@joincolumn(name="userid")}) private list<user> users;// 一个角色对应多个用户 }
3、permission
package com.example.shiro.entity; import lombok.getter; import lombok.setter; import javax.persistence.*; import java.util.list; @entity @getter @setter public class permission { @id @generatedvalue(strategy = generationtype.auto) private long permissionid;//主键. @column(nullable = false) private string permissionname;//名称. @column(columndefinition="enum('menu','button')") private string resourcetype;//资源类型,[menu|button] private string url;//资源路径. private string permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view private long parentid; //父编号 private string parentids; //父编号列表 private boolean available = boolean.true; //角色 -- 权限关系:多对多关系; @manytomany(fetch= fetchtype.eager) @jointable(name="rolepermission",joincolumns={@joincolumn(name="permissionid")},inversejoincolumns={@joincolumn(name="roleid")}) private list<role> roles; }
4、loginresult
package com.example.shiro.model; import lombok.data; @data public class loginresult { private boolean islogin = false; private string result; }
三、dao
1、添加一个dao基础接口:baserepository
package com.example.shiro.repository; import org.springframework.data.jpa.repository.jpaspecificationexecutor; import org.springframework.data.repository.norepositorybean; import org.springframework.data.repository.pagingandsortingrepository; import java.io.serializable; @norepositorybean public interface baserepository<t, i extends serializable> extends pagingandsortingrepository<t, i>, jpaspecificationexecutor<t> { }
2、userrepository
package com.example.shiro.repository; import com.example.shiro.entity.user; public interface userrepository extends baserepository<user,long> { user findbyusername(string username); }
四、service
1、loginservice
package com.example.shiro.service; import com.example.shiro.model.loginresult; public interface loginservice { loginresult login(string username, string password); void logout(); }
2、userservice
package com.example.shiro.service; import com.example.shiro.entity.user; public interface userservice { user findbyusername(string username); }
五、service.impl
1、loginserviceimpl
package com.example.shiro.service.impl; import com.example.shiro.model.loginresult; import com.example.shiro.repository.userrepository; import com.example.shiro.service.loginservice; import org.apache.shiro.securityutils; import org.apache.shiro.authc.authenticationexception; import org.apache.shiro.authc.incorrectcredentialsexception; import org.apache.shiro.authc.unknownaccountexception; import org.apache.shiro.authc.usernamepasswordtoken; import org.apache.shiro.session.session; import org.apache.shiro.subject.subject; import org.springframework.stereotype.service; @service public class loginserviceimpl implements loginservice { @override public loginresult login(string username, string password) { loginresult loginresult = new loginresult(); if (username == null || username.isempty()) { loginresult.setlogin(false); loginresult.setresult("用户名为空"); return loginresult; } string msg = ""; // 1、获取subject实例对象 subject currentuser = securityutils.getsubject(); // // 2、判断当前用户是否登录 // if (currentuser.isauthenticated() == false) { // // } // 3、将用户名和密码封装到usernamepasswordtoken usernamepasswordtoken token = new usernamepasswordtoken(username, password); // 4、认证 try { currentuser.login(token);// 传到myauthorizingrealm类中的方法进行认证 session session = currentuser.getsession(); session.setattribute("username", username); loginresult.setlogin(true); return loginresult; //return "/index"; } catch (unknownaccountexception e) { e.printstacktrace(); msg = "unknownaccountexception -- > 账号不存在:"; } catch (incorrectcredentialsexception e) { msg = "incorrectcredentialsexception -- > 密码不正确:"; } catch (authenticationexception e) { e.printstacktrace(); msg = "用户验证失败"; } loginresult.setlogin(false); loginresult.setresult(msg); return loginresult; } @override public void logout() { subject subject = securityutils.getsubject(); subject.logout(); } }
2、userserviceimpl
package com.example.shiro.service.impl; import com.example.shiro.entity.user; import com.example.shiro.repository.userrepository; import com.example.shiro.service.userservice; import org.springframework.stereotype.service; import javax.annotation.resource; @service public class userserviceimpl implements userservice { @resource private userrepository userrepository; @override public user findbyusername(string username) { return userrepository.findbyusername(username); } }
六、config配置类
1、创建realm
package com.example.shiro.config; import com.example.shiro.entity.permission; import com.example.shiro.entity.role; import com.example.shiro.entity.user; import com.example.shiro.service.userservice; import org.apache.shiro.authc.*; import org.apache.shiro.authz.authorizationinfo; import org.apache.shiro.authz.simpleauthorizationinfo; import org.apache.shiro.realm.authorizingrealm; import org.apache.shiro.subject.principalcollection; import org.apache.shiro.util.bytesource; import javax.annotation.resource; public class myshirorealm extends authorizingrealm { @resource private userservice userservice; /** * 身份认证:验证用户输入的账号和密码是否正确。 * */ @override protected authenticationinfo dogetauthenticationinfo(authenticationtoken token) throws authenticationexception { //获取用户输入的账号 string username = (string) token.getprincipal(); //通过username从数据库中查找 user对象. //实际项目中,这里可以根据实际情况做缓存,如果不做,shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 user user = userservice.findbyusername(username); if (user == null) { return null; } simpleauthenticationinfo authenticationinfo = new simpleauthenticationinfo( user,//这里传入的是user对象,比对的是用户名,直接传入用户名也没错,但是在授权部分就需要自己重新从数据库里取权限 user.getpassword(),//密码 bytesource.util.bytes(user.getcredentialssalt()),//salt=username+salt getname()//realm name ); return authenticationinfo; } /** * 权限信息 * */ @override protected authorizationinfo dogetauthorizationinfo(principalcollection principals) { simpleauthorizationinfo authorizationinfo = new simpleauthorizationinfo(); //如果身份认证的时候没有传入user对象,这里只能取到username //也就是simpleauthenticationinfo构造的时候第一个参数传递需要user对象 user user = (user)principals.getprimaryprincipal(); for(role role : user.getrolelist()){ //添加角色 authorizationinfo.addrole(role.getrole()); for(permission p:role.getpermissions()){ //添加权限 authorizationinfo.addstringpermission(p.getpermission()); } } return authorizationinfo; } }
2、配置shiro
package com.example.shiro.config; import org.apache.shiro.authc.credential.hashedcredentialsmatcher; import org.apache.shiro.spring.security.interceptor.authorizationattributesourceadvisor; import org.apache.shiro.spring.web.shirofilterfactorybean; import org.apache.shiro.web.mgt.defaultwebsecuritymanager; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import org.springframework.web.servlet.handler.simplemappingexceptionresolver; import java.util.hashmap; import java.util.map; import java.util.properties; @configuration public class shiroconfig { //将自己的验证方式加入容器 @bean myshirorealm myshirorealm() { myshirorealm myshirorealm = new myshirorealm(); myshirorealm.setcredentialsmatcher(hashedcredentialsmatcher()); return myshirorealm; } //权限管理,配置主要是realm的管理认证 @bean defaultwebsecuritymanager securitymanager() { defaultwebsecuritymanager manager = new defaultwebsecuritymanager(); manager.setrealm(myshirorealm()); return manager; } //凭证匹配器(密码校验交给shiro的simpleauthenticationinfo进行处理) @bean public hashedcredentialsmatcher hashedcredentialsmatcher(){ hashedcredentialsmatcher hashedcredentialsmatcher = new hashedcredentialsmatcher(); hashedcredentialsmatcher.sethashalgorithmname("md5");//散列算法:这里使用md5算法; hashedcredentialsmatcher.sethashiterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedcredentialsmatcher; } // filter工厂,设置对应的过滤条件和跳转条件 @bean shirofilterfactorybean shirofilterfactorybean() { shirofilterfactorybean bean = new shirofilterfactorybean(); bean.setsecuritymanager(securitymanager()); map<string, string> filtermap = new hashmap<string, string>(); // 登出 filtermap.put("/logout", "logout"); // swagger filtermap.put("/swagger**/**", "anon"); filtermap.put("/webjars/**", "anon"); filtermap.put("/v2/**", "anon"); // 对所有用户认证 filtermap.put("/**", "authc"); // 登录 bean.setloginurl("/login"); // 首页 bean.setsuccessurl("/index"); // 未授权页面,认证不通过跳转 bean.setunauthorizedurl("/403"); bean.setfilterchaindefinitionmap(filtermap); return bean; } //开启shiro aop注解支持. @bean public authorizationattributesourceadvisor authorizationattributesourceadvisor(){ authorizationattributesourceadvisor authorizationattributesourceadvisor = new authorizationattributesourceadvisor(); authorizationattributesourceadvisor.setsecuritymanager(securitymanager()); return authorizationattributesourceadvisor; } //shiro注解模式下,登录失败或者是没有权限都是抛出异常,并且默认的没有对异常做处理,配置一个异常处理 @bean(name="simplemappingexceptionresolver") public simplemappingexceptionresolver createsimplemappingexceptionresolver() { simplemappingexceptionresolver r = new simplemappingexceptionresolver(); properties mappings = new properties(); mappings.setproperty("databaseexception", "databaseerror");//数据库异常处理 mappings.setproperty("unauthorizedexception","/403"); r.setexceptionmappings(mappings); // none by default r.setdefaulterrorview("error"); // no default r.setexceptionattribute("exception"); // default is "exception" return r; } }
3、配置swagger
package com.example.shiro.config; import io.swagger.annotations.apioperation; import org.springframework.context.annotation.bean; import org.springframework.context.annotation.configuration; import springfox.documentation.builders.apiinfobuilder; import springfox.documentation.builders.pathselectors; import springfox.documentation.builders.requesthandlerselectors; import springfox.documentation.service.apiinfo; import springfox.documentation.service.contact; import springfox.documentation.spi.documentationtype; import springfox.documentation.spring.web.plugins.docket; import springfox.documentation.swagger2.annotations.enableswagger2; @configuration @enableswagger2 public class swaggerconfig { @bean public docket api() { return new docket(documentationtype.swagger_2) .apiinfo(apiinfo()) .select() .apis(requesthandlerselectors.any()) .paths(pathselectors.any()).build(); } private static apiinfo apiinfo() { return new apiinfobuilder() .title("api文档") .description("swagger api 文档") .version("1.0") .contact(new contact("name..", "url..", "email..")) .build(); } }
七、controller
1、logincontroller用来处理登录
package com.example.shiro.controller; import com.example.shiro.entity.user; import com.example.shiro.model.loginresult; import com.example.shiro.service.loginservice; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.postmapping; import org.springframework.web.bind.annotation.requestbody; import org.springframework.web.bind.annotation.restcontroller; import javax.annotation.resource; @restcontroller public class logincontroller { @resource private loginservice loginservice; @getmapping(value = "/login") public string login() { return "登录页"; } @postmapping(value = "/login") public string login(@requestbody user user) { system.out.println("login()"); string username = user.getusername(); string password = user.getpassword(); loginresult loginresult = loginservice.login(username,password); if(loginresult.islogin()){ return "登录成功"; } else { return "登录失败:" + loginresult.getresult(); } } @getmapping(value = "/index") public string index() { return "主页"; } @getmapping(value = "/logout") public string logout() { return "退出"; } @getmapping("/403") public string unauthorizedrole(){ return "没有权限"; } }
2、usercontroller用来测试访问,权限全部采用注解的方式。
package com.example.shiro.controller; import org.apache.shiro.authz.annotation.requirespermissions; import org.springframework.web.bind.annotation.getmapping; import org.springframework.web.bind.annotation.requestmapping; import org.springframework.web.bind.annotation.restcontroller; @restcontroller @requestmapping("/user") public class usercontroller { //用户查询 @getmapping("/userlist") @requirespermissions("user:view")//权限管理; public string userinfo(){ return "userlist"; } //用户添加 @getmapping("/useradd") @requirespermissions("user:add")//权限管理; public string userinfoadd(){ return "useradd"; } //用户删除 @getmapping("/userdel") @requirespermissions("user:del")//权限管理; public string userdel(){ return "userdel"; } }
八、数据库预设一些数据
先运行一遍程序,jpa生成数据库表后,手工执行sql脚本插入样本数据。
用户admin的密码是123456
insert into `user` (`userid`,`username`,`name`,`password`,`salt`,`state`) values ('1', 'admin', '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1); insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) values (1,1,'用户管理',0,'0/','user:view','menu','user/userlist'); insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) values (2,1,'用户添加',1,'0/1','user:add','button','user/useradd'); insert into `permission` (`permissionid`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) values (3,1,'用户删除',1,'0/1','user:del','button','user/userdel'); insert into `role` (`roleid`,`available`,`description`,`role`) values (1,1,'管理员','admin'); insert into `rolepermission` (`permissionid`,`roleid`) values (1,1); insert into `rolepermission` (`permissionid`,`roleid`) values (2,1); insert into `userrole` (`roleid`,`userid`) values (1,1);
九、swagger测试
1、启动项目,访问http://localhost:8080/swagger-ui.html
2、访问/user/useradd,系统返回到登录页
3、访问post的/login,请求参数输入:
{ "username": "admin", "password": "123456" }
response body显示登录成功。
4、再次访问/user/useradd,因为登录成功了并且有权限,这次response body显示useradd
5、访问/user/userdel,因为数据库没有配置权限,所以response body显示没有权限
推荐阅读
-
spring boot 2 集成JWT实现api接口认证
-
ASP.NET MVC+EF框架+EasyUI实现权限管理系列(2)-数据库访问层的设计Demo
-
怎么使用管理员权限运行CMD命令提示符(2种实现方法)
-
Spring boot 入门(四):集成 Shiro 实现登陆认证和权限管理
-
Spring boot security权限管理集成cas单点登录
-
Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
-
Spring Boot+Quartz实现一个实时管理的定时任务
-
Spring Boot+Spring Cloud+Vue+Element项目实战 手把手教你开发权限管理系统 徐丽健著 清华大学出版社
-
Spring Boot分布式系统实践【扩展1】shiro+redis实现session共享、simplesession反序列化失败的问题定位及反思改进
-
SSM+Shiro+Redis实现项目的权限管理