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; } }
说明:
这里使用@Getter,@Setter注解,不能使用@Data注解,因为实体使用了jpa的@oneToMany ,加载方式为lazy,在主表查询时关联表未加载,而主表使用@Data后会实现带关联表属性的hashCode和equals等方法。在运行过程中调用关联表数据时会显示异常 java.lang.stackoverflowerror。
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, Response body显示登录页
3、访问POST的/login,请求参数输入:
{ "userName": "admin", "password": "123456" }
Response body显示登录成功。
4、再次访问/user/userAdd,因为登录成功了并且有权限,这次Response body显示userAdd
5、访问/user/userDel,因为数据库没有配置权限,所以Response body显示没有权限