Spring Boot Security

不要亂摸發表於2018-06-08

如圖,是一種通用的使用者許可權模型。一般情況下會有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

 

相關文章