原文格式清晰,轉載自:https://www.cnblogs.com/cjsblog/p/9152455.html
如圖,是一種通用的用戶權限模型。一般情況下會有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