• Spring Boot Security


    如图,是一种通用的用户权限模型。一般情况下会有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=" 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

  • 相关阅读:
    (一)Python装饰器的通俗理解
    Linux实例安装VNC Server实现图形化访问
    TightVNC for Windows
    使用Xmanager远程CentOS 7服务器(XDMCP)
    Using Xmanager to connect to remote CentOS 7 via XDMCP
    在windows上使用xdmcp登陆centos,红帽linux
    Xmanger远程连接Centos7(成功配置)
    Centos7.2命令安装图形化界面
    CentOS 7安装图形界面
    CentOS 7命令行安装GNOME、KDE图形界面(成功安装验证)
  • 原文地址:https://www.cnblogs.com/mrchenzheng/p/12165729.html
Copyright © 2020-2023  润新知