Shiro是一個功能強大且易於使用的Java安全框架,主要功能有身份驗證、授權、加密和會話管理。 ...
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顯示沒有許可權