如圖,是一種通用的使用者許可權模型。一般情況下會有5張表,分別是:使用者表,角色表,許可權表,使用者角色關係表,角色許可權對應表。
一般,資源分配時是基於角色的(即,資源訪問許可權賦給角色,使用者通過角色進而擁有許可權);而訪問資源的時候是基於資源許可權去進行授權判斷的。
Spring Security和Apache Shiro是兩個應用比較多的許可權管理框架。Spring Security依賴Spring,其功能強大,相對於Shiro而言學習難度稍大一些。
Spring的強大是不言而喻的,可擴充套件性也很強,強大到用Spring家族的產品只要按照其推薦的做法來就非常非常簡單,否則,自己去整合過程可能會很痛苦。
目前,我們專案是基於Spring Boot的,而且Spring Boot的許可權管理也是推薦使用Spring Security的,所以再難也是要學習的。
Spring Security簡介
Spring Security致力於為Java應用提供認證和授權管理。它是一個強大的,高度自定義的認證和訪問控制框架。
具體介紹參見https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/
這句話包括兩個關鍵詞:Authentication(認證)和 Authorization(授權,也叫訪問控制)
認證是驗證使用者身份的合法性,而授權是控制你可以做什麼。
簡單地來說,認證就是你是誰,授權就是你可以做什麼。
在開始整合之前,我們先簡單瞭解幾個介面:
AuthenticationProvider
AuthenticationProvider介面是用於認證的,可以通過實現這個介面來定製我們自己的認證邏輯,它的實現類有很多,預設的是JaasAuthenticationProvider
它的全稱是 Java Authentication and Authorization Service (JAAS)
AccessDecisionManager
AccessDecisionManager是用於訪問控制的,它決定使用者是否可以訪問某個資源,實現這個介面可以定製我們自己的授權邏輯。
AccessDecisionVoter
AccessDecisionVoter是投票器,在授權的時通過投票的方式來決定使用者是否可以訪問,這裡涉及到投票規則。
UserDetailsService
UserDetailsService是用於載入特定使用者資訊的,它只有一個介面通過指定的使用者名稱去查詢使用者。
UserDetails
UserDetails代表使用者資訊,即主體,相當於Shiro中的Subject。User是它的一個實現。
Spring Boot整合Spring Security
按照官方文件的說法,為了定義我們自己的認證管理,我們可以新增UserDetailsService, AuthenticationProvider, or AuthenticationManager這種型別的Bean。
實現的方式有多種,這裡我選擇最簡單的一種(因為本身我們這裡的認證授權也比較簡單)
通過定義自己的UserDetailsService從資料庫查詢使用者資訊,至於認證的話就用預設的。
Maven依賴
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 3 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4 <modelVersion>4.0.0</modelVersion> 5 6 <groupId>com.cjs.example</groupId> 7 <artifactId>cjs-springsecurity-example</artifactId> 8 <version>0.0.1-SNAPSHOT</version> 9 <packaging>jar</packaging> 10 11 <name>cjs-springsecurity-example</name> 12 <description></description> 13 14 <parent> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-parent</artifactId> 17 <version>2.0.2.RELEASE</version> 18 <relativePath/> <!-- lookup parent from repository --> 19 </parent> 20 21 <properties> 22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 24 <java.version>1.8</java.version> 25 </properties> 26 27 <dependencies> 28 <dependency> 29 <groupId>org.springframework.boot</groupId> 30 <artifactId>spring-boot-starter-cache</artifactId> 31 </dependency> 32 <dependency> 33 <groupId>org.springframework.boot</groupId> 34 <artifactId>spring-boot-starter-data-redis</artifactId> 35 </dependency> 36 <dependency> 37 <groupId>org.springframework.boot</groupId> 38 <artifactId>spring-boot-starter-security</artifactId> 39 </dependency> 40 <dependency> 41 <groupId>org.springframework.boot</groupId> 42 <artifactId>spring-boot-starter-thymeleaf</artifactId> 43 </dependency> 44 <dependency> 45 <groupId>org.springframework.boot</groupId> 46 <artifactId>spring-boot-starter-web</artifactId> 47 </dependency> 48 <dependency> 49 <groupId>org.thymeleaf.extras</groupId> 50 <artifactId>thymeleaf-extras-springsecurity4</artifactId> 51 <version>3.0.2.RELEASE</version> 52 </dependency> 53 54 55 <dependency> 56 <groupId>org.projectlombok</groupId> 57 <artifactId>lombok</artifactId> 58 <optional>true</optional> 59 </dependency> 60 <dependency> 61 <groupId>org.springframework.boot</groupId> 62 <artifactId>spring-boot-starter-test</artifactId> 63 <scope>test</scope> 64 </dependency> 65 <dependency> 66 <groupId>org.springframework.security</groupId> 67 <artifactId>spring-security-test</artifactId> 68 <scope>test</scope> 69 </dependency> 70 </dependencies> 71 72 <build> 73 <plugins> 74 <plugin> 75 <groupId>org.springframework.boot</groupId> 76 <artifactId>spring-boot-maven-plugin</artifactId> 77 </plugin> 78 </plugins> 79 </build> 80 81 </project>
Security配置
1 package com.cjs.example.config; 2 3 import com.cjs.example.support.MyUserDetailsService; 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 9 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 12 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 import org.springframework.security.crypto.password.PasswordEncoder; 14 15 @Configuration 16 @EnableWebSecurity 17 @EnableGlobalMethodSecurity(prePostEnabled = true) // 啟用方法級別的許可權認證 18 public class SecurityConfig extends WebSecurityConfigurerAdapter { 19 20 @Autowired 21 private MyUserDetailsService myUserDetailsService; 22 23 24 @Override 25 protected void configure(HttpSecurity http) throws Exception { 26 // 允許所有使用者訪問"/"和"/index.html" 27 http.authorizeRequests() 28 .antMatchers("/", "/index.html").permitAll() 29 .anyRequest().authenticated() // 其他地址的訪問均需驗證許可權 30 .and() 31 .formLogin() 32 .loginPage("/login.html") // 登入頁 33 .failureUrl("/login-error.html").permitAll() 34 .and() 35 .logout() 36 .logoutSuccessUrl("/index.html"); 37 } 38 39 @Override 40 protected void configure(AuthenticationManagerBuilder auth) throws Exception { 41 auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); 42 } 43 44 @Bean 45 public PasswordEncoder passwordEncoder() { 46 return new BCryptPasswordEncoder(); 47 } 48 49 }
MyUserDetailsService
1 package com.cjs.example.support; 2 3 import com.cjs.example.entity.SysPermission; 4 import com.cjs.example.entity.SysRole; 5 import com.cjs.example.entity.SysUser; 6 import com.cjs.example.service.UserService; 7 import org.springframework.beans.factory.annotation.Autowired; 8 import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 import org.springframework.security.core.userdetails.User; 10 import org.springframework.security.core.userdetails.UserDetails; 11 import org.springframework.security.core.userdetails.UserDetailsService; 12 import org.springframework.security.core.userdetails.UsernameNotFoundException; 13 import org.springframework.stereotype.Service; 14 15 import java.util.ArrayList; 16 import java.util.List; 17 18 @Service 19 public class MyUserDetailsService implements UserDetailsService { 20 21 @Autowired 22 private UserService userService; 23 24 /** 25 * 授權的時候是對角色授權,而認證的時候應該基於資源,而不是角色,因為資源是不變的,而使用者的角色是會變的 26 */ 27 28 @Override 29 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 30 SysUser sysUser = userService.getUserByName(username); 31 if (null == sysUser) { 32 throw new UsernameNotFoundException(username); 33 } 34 List<SimpleGrantedAuthority> authorities = new ArrayList<>(); 35 for (SysRole role : sysUser.getRoleList()) { 36 for (SysPermission permission : role.getPermissionList()) { 37 authorities.add(new SimpleGrantedAuthority(permission.getCode())); 38 } 39 } 40 41 return new User(sysUser.getUsername(), sysUser.getPassword(), authorities); 42 } 43 }
許可權分配
1 package com.cjs.example.service.impl; 2 3 import com.cjs.example.dao.UserDao; 4 import com.cjs.example.entity.SysUser; 5 import com.cjs.example.service.UserService; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.cache.annotation.Cacheable; 8 import org.springframework.stereotype.Service; 9 10 @Service 11 public class UserServiceImpl implements UserService { 12 13 @Autowired 14 private UserDao userDao; 15 16 @Cacheable(cacheNames = "authority", key = "#username") 17 @Override 18 public SysUser getUserByName(String username) { 19 return userDao.selectByName(username); 20 } 21 }
1 package com.cjs.example.dao; 2 3 import com.cjs.example.entity.SysPermission; 4 import com.cjs.example.entity.SysRole; 5 import com.cjs.example.entity.SysUser; 6 import lombok.extern.slf4j.Slf4j; 7 import org.springframework.stereotype.Repository; 8 9 import java.util.Arrays; 10 11 @Slf4j 12 @Repository 13 public class UserDao { 14 15 private SysRole admin = new SysRole("ADMIN", "管理員"); 16 private SysRole developer = new SysRole("DEVELOPER", "開發者"); 17 18 { 19 SysPermission p1 = new SysPermission(); 20 p1.setCode("UserIndex"); 21 p1.setName("個人中心"); 22 p1.setUrl("/user/index.html"); 23 24 SysPermission p2 = new SysPermission(); 25 p2.setCode("BookList"); 26 p2.setName("圖書列表"); 27 p2.setUrl("/book/list"); 28 29 SysPermission p3 = new SysPermission(); 30 p3.setCode("BookAdd"); 31 p3.setName("新增圖書"); 32 p3.setUrl("/book/add"); 33 34 SysPermission p4 = new SysPermission(); 35 p4.setCode("BookDetail"); 36 p4.setName("檢視圖書"); 37 p4.setUrl("/book/detail"); 38 39 admin.setPermissionList(Arrays.asList(p1, p2, p3, p4)); 40 developer.setPermissionList(Arrays.asList(p1, p2)); 41 42 } 43 44 public SysUser selectByName(String username) { 45 log.info("從資料庫中查詢使用者"); 46 if ("zhangsan".equals(username)) { 47 SysUser sysUser = new SysUser("zhangsan", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); 48 sysUser.setRoleList(Arrays.asList(admin, developer)); 49 return sysUser; 50 }else if ("lisi".equals(username)) { 51 SysUser sysUser = new SysUser("lisi", "$2a$10$EIfFrWGINQzP.tmtdLd2hurtowwsIEQaPFR9iffw2uSKCOutHnQEm"); 52 sysUser.setRoleList(Arrays.asList(developer)); 53 return sysUser; 54 } 55 return null; 56 } 57 58 }
示例
這裡我設計的例子是使用者登入成功以後跳到個人中心,然後使用者可以可以進入圖書列表檢視。
使用者zhangsan可以檢視所有的,而lisi只能檢視圖書列表,不能新增不能檢視詳情。
頁面設計
LoginController.java
1 package com.cjs.example.controller; 2 3 import org.springframework.stereotype.Controller; 4 import org.springframework.ui.Model; 5 import org.springframework.web.bind.annotation.RequestMapping; 6 7 @Controller 8 public class LoginController { 9 10 // Login form 11 @RequestMapping("/login.html") 12 public String login() { 13 return "login.html"; 14 } 15 16 // Login form with error 17 @RequestMapping("/login-error.html") 18 public String loginError(Model model) { 19 model.addAttribute("loginError", true); 20 return "login.html"; 21 } 22 23 }
BookController.java
1 package com.cjs.example.controller; 2 3 import org.springframework.security.access.prepost.PreAuthorize; 4 import org.springframework.stereotype.Controller; 5 import org.springframework.web.bind.annotation.GetMapping; 6 import org.springframework.web.bind.annotation.RequestMapping; 7 8 @Controller 9 @RequestMapping("/book") 10 public class BookController { 11 12 @PreAuthorize("hasAuthority('BookList')") 13 @GetMapping("/list.html") 14 public String list() { 15 return "book/list"; 16 } 17 18 @PreAuthorize("hasAuthority('BookAdd')") 19 @GetMapping("/add.html") 20 public String add() { 21 return "book/add"; 22 } 23 24 @PreAuthorize("hasAuthority('BookDetail')") 25 @GetMapping("/detail.html") 26 public String detail() { 27 return "book/detail"; 28 } 29 }
UserController.java
1 package com.cjs.example.controller; 2 3 import com.cjs.example.entity.SysUser; 4 import com.cjs.example.service.UserService; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.security.access.prepost.PreAuthorize; 7 import org.springframework.stereotype.Controller; 8 import org.springframework.web.bind.annotation.GetMapping; 9 import org.springframework.web.bind.annotation.RequestMapping; 10 import org.springframework.web.bind.annotation.ResponseBody; 11 12 @Controller 13 @RequestMapping("/user") 14 public class UserController { 15 16 @Autowired 17 private UserService userService; 18 19 /** 20 * 個人中心 21 */ 22 @PreAuthorize("hasAuthority('UserIndex')") 23 @GetMapping("/index") 24 public String index() { 25 return "user/index"; 26 } 27 28 @RequestMapping("/hi") 29 @ResponseBody 30 public String hi() { 31 SysUser sysUser = userService.getUserByName("zhangsan"); 32 return sysUser.toString(); 33 } 34 35 }
index.html
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>首頁</title> 6 </head> 7 <body> 8 <h2>這裡是首頁</h2> 9 </body> 10 </html>
login.html
1 <!DOCTYPE html> 2 <html lang="zh" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Login page</title> 6 </head> 7 <body> 8 <h1>Login page</h1> 9 <p th:if="${loginError}" class="error">使用者名稱或密碼錯誤</p> 10 <form th:action="@{/login.html}" method="post"> 11 <label for="username">Username</label>: 12 <input type="text" id="username" name="username" autofocus="autofocus" /> <br /> 13 <label for="password">Password</label>: 14 <input type="password" id="password" name="password" /> <br /> 15 <input type="submit" value="Login" /> 16 </form> 17 </body> 18 </html>
/user/index.html
1 <!DOCTYPE html> 2 <html lang="zh" xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>個人中心</title> 6 </head> 7 <body> 8 <h2>個人中心</h2> 9 <div th:insert="~{fragments/header::logout}"></div> 10 <a href="/book/list.html">圖書列表</a> 11 </body> 12 </html>
/book/list.html
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> <meta charset="UTF-8"> <title>圖書列表</title> </head> <body> <div th:insert="~{fragments/header::logout}"></div> <h2>圖書列表</h2> <div sec:authorize="hasAuthority('BookAdd')"> <button onclick="">新增</button> </div> <table border="1" cellspacing="0" style="width: 20%"> <thead> <tr> <th>名稱</th> <th>出版社</th> <th>價格</th> <th>操作</th> </tr> </thead> <tbody> <tr> <td>Java從入門到放棄</td> <td>機械工業出版社</td> <td>39</td> <td><span sec:authorize="hasAuthority('BookDetail')"><a href="/book/detail.html">檢視</a></span></td> </tr> <tr> <td>MySQ從刪庫到跑路</td> <td>清華大學出版社</td> <td>59</td> <td><span sec:authorize="hasAuthority('BookDetail')"><a href="/book/detail.html">檢視</a></span></td> </tr> </tbody> </table> </body> </html>
header.html
1 <!DOCTYPE html> 2 <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> 3 <body> 4 <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> 5 Logged in user: <span sec:authentication="name"></span> | 6 Roles: <span sec:authentication="principal.authorities"></span> 7 <div> 8 <form action="#" th:action="@{/logout}" method="post"> 9 <input type="submit" value="退出" /> 10 </form> 11 </div> 12 </div> 13 </body> 14 </html>
錯誤處理
ErrorController.java
1 package com.cjs.example.controller; 2 3 import lombok.extern.slf4j.Slf4j; 4 import org.springframework.http.HttpStatus; 5 import org.springframework.ui.Model; 6 import org.springframework.web.bind.annotation.ControllerAdvice; 7 import org.springframework.web.bind.annotation.ExceptionHandler; 8 import org.springframework.web.bind.annotation.ResponseStatus; 9 10 @Slf4j 11 @ControllerAdvice 12 public class ErrorController { 13 14 @ExceptionHandler(Throwable.class) 15 @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 16 public String exception(final Throwable throwable, final Model model) { 17 log.error("Exception during execution of SpringSecurity application", throwable); 18 String errorMessage = (throwable != null ? throwable.getMessage() : "Unknown error"); 19 model.addAttribute("errorMessage", errorMessage); 20 return "error"; 21 } 22 23 }
error.html
1 <!DOCTYPE html> 2 <html xmlns:th="http://www.thymeleaf.org"> 3 <head> 4 <title>Error page</title> 5 <meta charset="utf-8" /> 6 </head> 7 <body th:with="httpStatus=${T(org.springframework.http.HttpStatus).valueOf(#response.status)}"> 8 <h1 th:text="|${httpStatus} - ${httpStatus.reasonPhrase}|">404</h1> 9 <p th:utext="${errorMessage}">Error java.lang.NullPointerException</p> 10 <a href="index.html" th:href="@{/index.html}">返回首頁</a> 11 </body> 12 </html>
效果演示
zhangsan登入
lisi登入
至此,可以實現基本的許可權管理
工程結構
程式碼已上傳至https://github.com/chengjiansheng/cjs-springsecurity-example.git
訪問控制表示式
其它
通常情況下登入成功或者失敗以後不是跳轉到頁面而是返回json資料,該怎麼做呢?
可以繼承SavedRequestAwareAuthenticationSuccessHandler,並在配置中指定successHandler或者繼承SimpleUrlAuthenticationFailureHandler,並在配置中指定failureHandler
1 package com.cjs.example.handler; 2 3 import org.springframework.security.core.Authentication; 4 import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 5 6 import javax.servlet.ServletException; 7 import javax.servlet.http.HttpServletRequest; 8 import javax.servlet.http.HttpServletResponse; 9 import java.io.IOException; 10 import java.util.HashMap; 11 12 public class MySavedRequestAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { 13 @Override 14 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { 15 16 // // Use the DefaultSavedRequest URL 17 // String targetUrl = savedRequest.getRedirectUrl(); 18 // logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl); 19 // getRedirectStrategy().sendRedirect(request, response, targetUrl); 20 21 Map<String, Object> map = new HashMap<>(); 22 response.getWriter().write(JSON.toJSONString(map)); 23 24 25 } 26 }
這麼複雜感覺還不如自己寫個Filter還簡單些
是的,僅僅是這些的話還真不如自己寫個過濾器來得簡單,但是Spring Security的功能遠不止如此,比如OAuth2,CSRF等等
這個只適用單應用,不可能每個需要許可權的系統都這麼去寫,可以不可以做成認證中心,做單點登入?
當然是可以的,而且必須可以。許可權分配可以用一個管理後臺,認證和授權必須獨立出來,下一節用OAuth2.0來實現
參考
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#el-pre-post-annotations
https://docs.spring.io/spring-security/site/docs/5.0.5.RELEASE/reference/htmlsingle/#getting-started
https://www.thymeleaf.org/doc/articles/standarddialect5minutes.html
https://www.thymeleaf.org/doc/articles/layouts.html
https://www.thymeleaf.org/doc/articles/springsecurity.html
https://blog.csdn.net/u283056051/article/details/55803855
https://segmentfault.com/a/1190000008893479
https://www.bbsmax.com/A/A2dmY2DWde/
https://blog.csdn.net/qq_29580525/article/details/79317969