在springboot中結合shiro教程搭建權限管理,其中幾個小細節的地方對新手不友好,伸手黨更是沒法直接運行代碼,搭建過程容易遇坑,記錄一下。關鍵的地方也給註釋了。css
版本:springboot版本2.x,shiro1.4html
1、依賴前端
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>java
2、實體mysql
實體有三個,根據規則會自動生成兩個中間表,數據庫實際上會生成5張表。git
分別是User,SysRole,SysPermission,中間表按照@JoinTable來生成。github
@Entity public class User { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer 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 = "SysUserRole", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns ={@JoinColumn(name = "roleId") }) private List<SysRole> roleList;// 一個用戶具備多個角色 @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm") private LocalDateTime createTime;//建立時間 @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate expiredDate;//過時日期 private String email; public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public LocalDateTime getCreateTime() { return createTime; } public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; } public LocalDate getExpiredDate() { return expiredDate; } public void setExpiredDate(LocalDate expiredDate) { this.expiredDate = expiredDate; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } 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 String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public byte getState() { return state; } public void setState(byte state) { this.state = state; } public List<SysRole> getRoleList() { return roleList; } public void setRoleList(List<SysRole> roleList) { this.roleList = roleList; } /** * 密碼鹽. 從新對鹽從新進行了定義,用戶名+salt,這樣就不容易被破解,能夠採用多種方式定義加鹽 * @return */ public String getCredentialsSalt(){ return this.userName+this.salt; } }
@Entity public class SysRole { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer 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="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")}) private List<SysPermission> permissions; // 用戶 - 角色關係定義; @ManyToMany @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="userId")}) private List<User> users;// 一個角色對應多個用戶 public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } public String getRole() { return role; } public void setRole(String role) { this.role = role; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Boolean getAvailable() { return available; } public void setAvailable(Boolean available) { this.available = available; } public List<SysPermission> getPermissions() { return permissions; } public void setPermissions(List<SysPermission> permissions) { this.permissions = permissions; } public List<User> getUsers() { return users; } public void setUsers(List<User> users) { this.users = users; } }
@Entity public class SysPermission { @Id @GenericGenerator(name="generator",strategy = "native") @GeneratedValue(generator = "generator") private Integer 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 @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")}) private List<SysRole> roles; public Integer getPermissionId() { return permissionId; } public void setPermissionId(Integer permissionId) { this.permissionId = permissionId; } public String getPermissionName() { return permissionName; } public void setPermissionName(String permissionName) { this.permissionName = permissionName; } public String getResourceType() { return resourceType; } public void setResourceType(String resourceType) { this.resourceType = resourceType; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public String getPermission() { return permission; } public void setPermission(String permission) { this.permission = permission; } public Long getParentId() { return parentId; } public void setParentId(Long parentId) { this.parentId = parentId; } public String getParentIds() { return parentIds; } public void setParentIds(String parentIds) { this.parentIds = parentIds; } public Boolean getAvailable() { return available; } public void setAvailable(Boolean available) { this.available = available; } public List<SysRole> getRoles() { return roles; } public void setRoles(List<SysRole> roles) { this.roles = roles; } }
3、DAO,這裏用JPAweb
public interface UserRepository extends JpaRepository<User,Integer> { User findByUserName(String userName); }
依賴是算法
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
4、Servicespring
public interface UserService { User findByUserName(String userName); } public interface LoginService { LoginResult login(String userName,String password); void logout(); }
5、Service.impl
@Service public class UserServiceImpl implements UserService { @Resource private UserRepository userRepository; @Override public User findByUserName(String userName) { return userRepository.findByUserName(userName); } }
//內部使用的一個model,根據須要擴展
public class LoginResult { private boolean isLogin = false; private String result; public boolean isLogin() { return isLogin; } public void setLogin(boolean login) { isLogin = login; } public String getResult() { return result; } public void setResult(String result) { this.result = result; } }
@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=""; // 一、獲取Subject實例對象 Subject currentUser = SecurityUtils.getSubject(); // // 二、判斷當前用戶是否登陸 // if (currentUser.isAuthenticated() == false) { // // } // 三、將用戶名和密碼封裝到UsernamePasswordToken UsernamePasswordToken token = new UsernamePasswordToken(userName, password); // 四、認證 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(); } }
6、config,配置類
shiro核心配置
public class MyShiroRealm extends AuthorizingRealm { @Resource private UserService userService; //權限信息,包括角色以及權限 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("權限配置-->MyShiroRealm.doGetAuthorizationInfo()"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //若是身份認證的時候沒有傳入User對象,這裏只能取到userName //也就是SimpleAuthenticationInfo構造的時候第一個參數傳遞須要User對象 User user = (User)principals.getPrimaryPrincipal(); for(SysRole role:user.getRoleList()){ authorizationInfo.addRole(role.getRole()); for(SysPermission p:role.getPermissions()){ authorizationInfo.addStringPermission(p.getPermission()); } } return authorizationInfo; } /*主要是用來進行身份認證的,也就是說驗證用戶輸入的帳號和密碼是否正確。*/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("MyShiroRealm.doGetAuthenticationInfo()"); //獲取用戶的輸入的帳號. String userName = (String)token.getPrincipal(); System.out.println(token.getCredentials()); //經過username從數據庫中查找 User對象. //實際項目中,這裏能夠根據實際狀況作緩存,若是不作,Shiro本身也是有時間間隔機制,2分鐘內不會重複執行該方法 User user = userService.findByUserName(userName); System.out.println("----->>user="+user); 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; } }
@Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); //攔截器. Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>(); // 配置不會被攔截的連接 順序判斷,由於前端模板採用了thymeleaf,這裏不能直接使用 ("/static/**", "anon")來配置匿名訪問,必須配置到每一個靜態目錄 filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/fonts/**", "anon"); filterChainDefinitionMap.put("/img/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/html/**", "anon"); //配置退出 過濾器,其中的具體的退出代碼Shiro已經替咱們實現了 filterChainDefinitionMap.put("/logout", "logout"); //<!-- 過濾鏈定義,從上向下順序執行,通常將/**放在最爲下邊 -->:這是一個坑呢,一不當心代碼就很差使了; //<!-- authc:全部url都必須認證經過才能夠訪問; anon:全部url都均可以匿名訪問--> filterChainDefinitionMap.put("/**", "authc"); // 若是不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登陸成功後要跳轉的連接 shiroFilterFactoryBean.setSuccessUrl("/index"); //未受權界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 憑證匹配器 * (因爲咱們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * ) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這裏使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次數,好比散列兩次,至關於 md5(md5("")); return hashedCredentialsMatcher; } @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myShiroRealm()); return securityManager; } /** * 開啓shiro aop註解支持. * 使用代理方式;因此須要開啓代碼支持; * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } @Bean(name="simpleMappingExceptionResolver") public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() { SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver(); Properties mappings = new Properties(); mappings.setProperty("DatabaseException", "databaseError");//數據庫異常處理 mappings.setProperty("UnauthorizedException","/user/403"); r.setExceptionMappings(mappings); // None by default r.setDefaultErrorView("error"); // No default r.setExceptionAttribute("exception"); // Default is "exception" //r.setWarnLogCategory("example.MvcLogger"); // No default return r; } }
7、controller
HomeController用來處理登陸
@Controller public class HomeController { @Resource private LoginService loginService; @RequestMapping({"/","/index"}) public String index(){ return"/index"; } @RequestMapping("/403") public String unauthorizedRole(){ System.out.println("------沒有權限-------"); return "/user/403"; } @RequestMapping(value = "/login",method = RequestMethod.GET) public String toLogin(Map<String, Object> map,HttpServletRequest request) { loginService.logout(); return "/user/login"; } @RequestMapping(value = "/login",method = RequestMethod.POST) public String login(Map<String, Object> map,HttpServletRequest request) throws Exception{ System.out.println("login()"); String userName = request.getParameter("userName"); String password = request.getParameter("password"); LoginResult loginResult = loginService.login(userName,password); if(loginResult.isLogin()) { return "/index"; } else { map.put("msg",loginResult.getResult()); map.put("userName",userName); return "/user/login"; } } @RequestMapping("/logout") public String logOut(HttpSession session) { loginService.logout(); return "/user/login"; } }
UserController用來測試訪問,權限所有采用註解的方式。
@Controller @RequestMapping("/user") public class UserController { /** * 用戶查詢. * @return */ @RequestMapping("/userList") @RequiresPermissions("user:view")//權限管理; public String userInfo(){ return "userList"; } /** * 用戶添加; * @return */ @RequestMapping("/userAdd") @RequiresPermissions("user:add")//權限管理; public String userInfoAdd(){ return "userAdd"; } /** * 用戶刪除; * @return */ @RequestMapping("/userDel") @RequiresPermissions("user:del")//權限管理; public String userDel(){ return "userDel"; } }
8、html
Login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}"></link> <title>用戶登陸</title> <style> .bgColor{ background-color:rgba(243,66,111,0.15) } .divBorder{ border: solid 1px rgba(12,24,255,0.15); padding: 10px; margin-top: 10px; border-radius: 10px; text-align: center; vertical-align: middle; } .h4font{ margin-top: 0px; font-family: 微軟雅黑; font-weight: 500; } .center { padding: 20% 0; } </style> </head> <body> <div class="container"> <div class="row center"> <div class="divBorder col-sm-offset-3 col-sm-6"> <h3 class="panel panel-heading h4font"> 用戶登陸 </h3> <h4 th:text="${msg}"></h4> <form class="form-horizontal" th:action="@{/login}" method="post"> <div class="input-group input-group-lg"> <span class="input-group-addon"><i class="glyphicon glyphicon-user" aria-hidden="true"></i></span> <input type="text" class="form-control" id="userName" name="userName" placeholder="請輸入用戶名稱" th:value="${userName}"/> </div> <br> <div class="input-group input-group-lg"> <span class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></span> <input type="password" class="form-control" id="password" name="password" placeholder="請輸入密碼"/> </div> <br> <input type="submit" class="btn btn-lg btn-block btn-info" value="登 錄"> </form> </div> </div> </div> </body> </html>
9、數據庫裏預設一些數據
注意admin的密碼是123456,這裏保存的是加密後的密碼,根據前面的設置,是md5,散列2次。
登陸的時候shiro會根據配置自動給密碼123456加密,而後與數據庫裏取出的密碼比對。
注意先運行一遍程序,JPA生成數據庫表後,手工執行sql腳本插入樣本數據。
INSERT INTO `user` (`userId`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (1,0,'用戶管理',0,'0/','user:view','menu','user/userList'); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (2,0,'用戶添加',1,'0/1','user:add','button','user/userAdd'); INSERT INTO `syspermission` (`permissionId`,`available`,`permissionname`,`parentid`,`parentids`,`permission`,`resourcetype`,`url`) VALUES (3,0,'用戶刪除',1,'0/1','user:del','button','user/userDel'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (1,0,'管理員','admin'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (2,0,'VIP會員','vip'); INSERT INTO `sysrole` (`roleid`,`available`,`description`,`role`) VALUES (3,1,'test','test'); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (1,1); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (2,1); INSERT INTO `sysrolepermission` (`permissionid`,`roleid`) VALUES (3,2); INSERT INTO `sysuserrole` (`roleid`,`userId`) VALUES (1,1);
10、其餘配置
Application.yml
server: servlet: context-path: / port: 80 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/crmData?characterEncoding=utf8&useSSL=false username: root password: root jpa: hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl #按字段名字建表 show-sql: true database: mysql database-platform: org.hibernate.dialect.MySQL5InnoDBDialect thymeleaf: cache: false messages: basename: myconfig
全部依賴:
<dependencies>
<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>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
<!--<version>3.0.1.RELEASE</version>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
目錄圖:
其餘的html頁面本身隨便生成就能夠。
到此基本實現簡單的權限管理。
代碼能夠參考這個地址:https://github.com/asker124143222/bus