《深入理解 Spring Cloud 与微服务构建》第十六章 Spring Boot Security 详解

文章目录

  • 《深入理解 Spring Cloud 与微服务构建》第十六章 Spring Boot Security 详解
  • 一、Spring Security 简介
    • 1.什么是 Spring Security
    • 2.为什么选择 Spring Security
    • 3.Spring Security 提供的安全模块
  • 二、Spring Boot Security 与 Spring Security 的关系
  • 三、Spring Boot Security 案例详解
    • 1.构建 Spring Boot Security 工程
    • 2.配置 Spring Security
    • 3.编写相关界面
    • 4.Spring Security 方法级别上的保护
    • 5.从数据库中读取用户的认证信息
  • 四、总结

一、Spring Security 简介

1.什么是 Spring Security

Spring Security 是 Spring Resource 社区的一个安全组件,Spring Security 为 JavaEE 企业级开发提供了全面的安全防护。安全防护是一个不断变化的目标,Spring Security 通过版本不断迭代来实现这一目标。Spring Security 采用 “安全层” 的概念,使每一层都尽可能安全,连续的安全层可以达到全面的防护。Spring Security 可以在 Controller 层、Service 层、DAO 层等以加注解的方式来保护应用程序的安全。Spring Security 提供了细粒度的权限控制,可以精细到每一个 API 接口、每一个业务的方法,或者每一个操作数据库的 DAO 层的方法。Spring Security 提供的是应用程序层的安全解决方案,一个系统的安全还需要考虑传输层和系统层的安全,例如采用 HTTPS 协议、服务器部署防火墙、服务器集群隔离部署等

2.为什么选择 Spring Security

使用 Spring Security 有很多原因,其中一个重要原因是它对环境的无依赖性、低代码耦合性。将工程重新部署到一个新的服务器上,不需要为 Spring Security 做什么工作。Spring Security 提供了数十个安全模块,模块与模块间的耦合性低,模块之间可以自由组合来实现特定需求的安全功能,具有较高的可定制性。总而言之,Spring Security 具有很好地可复用性和可定制性

在安全方面,有两个主要的领域,一是 “认证”,即你是谁;二是 “授权”,即你拥有什么权限,Spring Security 的主要目标就是在这两个领域。“认证” 是认证主体的过程,通常是指可以在应用程序中执行操作的用户、设备或其他系统。“授权” 是指决定是否允许已认证的主体执行某一项操作

安全框架多种多样,那为什么选择 Spring Security 作为微服务开发的安全框架呢?JavaEE 有另一个优秀的安全框架 Apache Shiro,Apache Shiro 在企业级的项目开发中十分受欢迎,一般使用在单体服务中。Spring Security 来自 Spring Resource 社区,采用了注解的方式控制权限,熟悉 Spring 的开发者很容易上手 Spring Security。还有一个原因就是 Spring Security 易于应用于 Spring Boot 工程,也易于集成到采用 Spring Cloud 构建的微服务系统中

3.Spring Security 提供的安全模块

在安全验证方面,Spring Security 提供了很多的安全验证模块。大部分的验证模块来自第三方的权威机构或者一些相关的标准指定组织,Spring Security 自身也提供了一些验证模型。Spring Security 目前支持对以下技术的整合。(注:这部分内容来自 Spring Security 官方文档)

  • HTTP BASIC 头认证(一个基于 IETF RFC 的标准)
  • HTTP Digest 头认证(一个基于 IETF RFC 的标准)
  • HTTP X.509 客户端证书交换认证(一个基于 IETF RFC 的标准)
  • LDAP(一种通用的跨平台身份验证,特别是在大型软件架构中)
  • 基于表单的验证
  • OpenID 验证
  • 基于预先建立的请求头的验证
  • Jasig Central Authentication Service,也被称作 CAS,是一个流行的开源单点登录系统
  • 远程方法调用(RMI)和 HttpInvoker(Spring 远程协议)的认证
  • 自动 “记住我” 的身份验证
  • 匿名验证(允许每一次未经身份验证的调用)
  • Run-as 身份验证(每一次调用都需要提供身份标识)
  • Java 认证和授权服务
  • Java EE 容器认证
  • Kerberos
  • Java 开源的单点登录*
  • OpenNMS 网络管理平台*
  • AppFuse*
  • AndroMDA*
  • Mule ESB*
  • Direct Web Request(DWR)*
  • Grails*
  • Tapestry*
  • JTrac*
  • Jasypt*
  • Roller*
  • Elastic Path*
  • Atlassian Crowd*
  • 自己创建的认证系统

以上都是 Spring Security 支持的安全验证模块,其中带 * 的是来自第三方的安全验证模块,Spring Security 对这些模块做了整合和封装

二、Spring Boot Security 与 Spring Security 的关系

在 Spring Security 框架中,主要包含了两个依赖 Jar,分别是 spring-security-web 依赖和 spring-securtiy-config 依赖,代码如下:

<dependencies><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId><version>4.2.2.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId><version>4.2.2.RELEASE</version></dependency>
</dependencies>

Spring Boot 对 Spring Security 框架做了封装,仅仅是封装,并没有改动 Spring Security 这两个包的内容,并加上了 Spring Boot 的起步依赖的特性。spring-boot-starter-security 依赖如下:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
</dependencies>

进入 spring-boot-starter-securtiy 的 pom 文件,可以发现 pom 文件包含了 Spring Security 的两个 Jar 包,并移除了这两个 Jar 包的 apo 功能,引入了 apo 的依赖,另外包含了 spring-boot-starter 的依赖。由此可见,spring-boot-starter-security 是对 Spring Security 的一个封装

三、Spring Boot Security 案例详解

1.构建 Spring Boot Security 工程

在工程的 pom 文件中引入相关依赖,包括 2.1.0 的 Spring Boot 的起步依赖、Security 的起步依赖 spring-boot-starter-security、Web 模板引擎 Thymeleaf 的起步依赖 spring-boot-starter-thymeleaf、Web 功能的起步依赖 spring-boot-starter-web、Thymeleaf 和 Security 的依赖 thymeleaf-extras-springsecurity4。完整的 pom 依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>Security</artifactId><version>1.0-SNAPSHOT</version><parent><artifactId>spring-boot-starter-parent</artifactId><groupId>org.springframework.boot</groupId><version>2.1.0.RELEASE</version></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity4</artifactId><version>3.0.4.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

2.配置 Spring Security

创建完 Spring Boot 工程并引入工程所需的依赖后,需要配置 Spring Security。新建一个 SecurityConfig 类,作为配置类,它继承了 WebSecurityConfigurerAdapter 类。在 SecurityConfig 类上加 @EnableWebSecurity 注解,开启 WebSecurity 的功能,并需要注入 AuthenticationManagerBuilder 类的 Bean。代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("sisyphus").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");}
}

上述代码做了 Spring Security 的基本配置,并通过 AuthenticationManagerBuilder 在内存中创建了一个认证用户的信息,该认证用户名为 sisyphus,密码为 123456,有 USER 的角色。需要注意的是,密码需要用 PasswordEncoder 去加密,比如本案例中使用的 BcryptPasswordEncoder。读者也可以自定义 PasswordEncoder,之前的版本密码可以不用加密。上述的代码内容虽少,但做了很多安全防护的工作,包括以下内容:

  • 应用的每一个请求都需要认证
  • 自动生成了一个登录表单
  • 可以用 username 和 password 来进行认证
  • 用户可以注销
  • 阻止了 CSRF 攻击
  • Session Fixation 保护
  • 安全 Header 集成了以下内容:
    • HTTP Strict Transport Security for secure requests
    • X-Content-Type-Options integration
    • Cache Control
    • X-XSS-Protection integration
    • XFrame-Options integration to help prevent Clickjacking
  • 集成了以下的 ServletAPI 的方法
    • HttpServletRequest#getRemoteUser()
    • HttpServletRequest.html#getUserPrincipal()
    • HttpServletRequest.html#isUserInRole(java.lang.String)
    • HttpServletRequest.html#login(java.lang.String.java.lang.String)
    • HttpServletRequest.html#logout()

WebSecurityConfigureAdapter 配置了如何验证用户信息。那么 Spring Security 如何直到是否所有的用户都需要身份验证呢?又如何直到要支持基于表单的身份验证呢?工程的哪些资源需要验证,哪些资源不需要验证?这时就需要配置 HttpSecurity,通过重写 configure(HttpSecurity http) 方法来配置 HttpSecurity。代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/css/**", "/index").permitAll().antMatchers("/user/**").hasRole("USER").antMatchers("/blogs/**").hasRole("USER").and().formLogin().loginPage("/login").failureUrl("/login-error").and().exceptionHandling().accessDeniedPage("/401");http.logout().logoutSuccessUrl("/");}@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("sisyphus").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");}
}

在上述代码中,配置了如下内容:

  • 以 “/css/**” 开头的资源和 “/index” 资源不需要验证,外界请求可以直接访问这些资源
  • 以 “/user/**” 和 “/blogs/**” 开头的资源需要验证,并且需要用户的角色是 “USER”
  • 表单登录的地址是 “/login”,登录失败的地址是 “/login-error”
  • 异常处理会重定向到 “/401” 界面
  • 注销登录成功,重定向到首页

在上述的配置代码中配置了相关的界面,例如首页、登录页、用户首页等。配置这些界面在 Controller 层的代码如下:

package com.sisyphus.controller;import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;@Controller
public class MainController {@RequestMapping("/")public String root() {return "redirect:/index";}@RequestMapping("/index")public String index() {return "index";}@RequestMapping("/user/index")public String userIndex() {return "user/index";}@RequestMapping("/login")public String login() {return "login";}@RequestMapping("/login-error")public String loginError(Model model) {model.addAttribute("loginError", true);return "login";}@GetMapping("/401")public String accessDenied() {return "401";}
}

3.编写相关界面

在上一节中配置了相关的界面,因为界面只是为了演示 Spring Boot Security 的案例,并不是本章的重点,所以界面做得非常简单

在工程的配置文件 application.yml 中配置 thymeleaf 引擎,模式为 HTML5,编码为 UTF-8,配置代码如下:

server:port: 8080spring:thymeleaf:mode: HTML5encoding: UTF-8cache: false

目录结构如下:

main.css 样式文件如下:

登录界面(login.html)的代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"><head><title>Login page</title><meta charset="utf-8" /><link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" /></head><body><h1>Login page</h1><p>User角色用户: sisyphus / 123456</p><p>Admin角色用户: admin / 123456</p><p th:if="${loginError}" class="error">用户名或密码错误</p><form th:action="@{/login}" method="post"><label for="username">用户名</label>:<input type="text" id="username" name="username" autofocus="autofocus" /> <br /><label for="password">密码</label>:<input type="password" id="password" name="password" /> <br /><input type="submit" value="登录" /></form><p><a href="/index" th:href="@{/index}">返回首页</a></p></body>
</html>

首页(index.html)的代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"><head><title>Hello Spring Security</title><meta charset="utf-8" /><link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" /></head><body><h1>Hello Spring Security</h1><p>这个界面没有受保护,你可以进已被保护的界面.</p><div th:fragment="logout"  sec:authorize="isAuthenticated()">登录用户: <span sec:authentication="name"></span> |用户角色: <span sec:authentication="principal.authorities"></span><div><form action="#" th:action="@{/logout}" method="post"><input type="submit" value="登出" /></form></div></div><ul><li>点击<a href="/user/index" th:href="@{/user/index}">去/user/index保护的界面</a></li></ul></body>
</html>

权限不够显示的界面(401.html)代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"><body><div ><div ><h2> 权限不够</h2></div><div sec:authorize="isAuthenticated()"><p>已有用户登录</p><p>用户: <span sec:authentication="name"></span></p><p>角色: <span sec:authentication="principal.authorities"></span></p></div><div sec:authorize="isAnonymous()"><p>未有用户登录</p></div><p>拒绝访问!</p></div></body>
</html>

用户首页(/user.index.html)界面,该资源被 Spring Security 保护,只有拥有 “USER” 角色的用户才能够访问, 其代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"><head><title>Hello Spring Security</title><meta charset="utf-8" /><link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" /></head><body><div th:substituteby="index::logout"></div><h1>这个界面是被保护的界面</h1><p><a href="/index" th:href="@{/index}">返回首页</a></p><p><a  href="/blogs" th:href="@{/blogs}">管理博客</a></p></body>
</html>

启动工程,在浏览器上访问 localhost:8080,会被重定向到 localhost:8080/index 界面,如图所示:

单击 “去 /user/index 保护的界面”,由于 “/user/index” 界面需要 “USER” 权限,但还没有登录,会被重定向到登录界面 “/login.html”,登录界面如图所示:

这时,用具有 “USER” 角色的用户登录,即用户名为 sisyphus,密码为 123456。登录成功,界面会被重定向到 http://localhost:8080/user/index 界面,注意该界面是具有 “USER” 角色的用户才具有访问权限。界面显示如图所示:

为了演示 “/user/index” 界面只有 “USER” 角色才能访问,新建一个 admin 用户,该用户只有 “ADMIN” 的角色,没有 “USER” 角色,所以没有权限访问 “/user.index” 界面。修改 SecurityConfig 配置类,在这个类新增一个用户 admin,代码如下:

public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("sisyphus").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
}

InMemoryUserDetailsManager 类是将用户信息存放在程序的内存中的。程序启动后,InMemoryUserDetailsManager 会在内存中创建用户的信息。在上述的案例中创建两个用户,sisyphus 用户具有 “USER” 角色,admin 用户具有 “ADMIN” 角色。用 admin 用户去登录,并访问 http://localhost:8080/user/index,这时会被重定向到权限不足的界面,显示的界面如图所示:

这时给 admin 用户加上 “USER” 角色,修改 SecurityConfig 配置类的代码,具体代码如下:

auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN", "USER");

再次用 admin 用户访问 http://localhost:8080/user/index 界面,界面可以正常显示。可见 Spring Security 对 “/user/index” 资源进行了保护,并且只允许具有 “USER” 角色权限的用户访问

4.Spring Security 方法级别上的保护

Spring Security 从 2.0 版本开始,提供了方法级别的安全支持,并提供了 JSR-250 的支持。在配置类 SecurityConfig 上添加相关注解,就可以开启方法级别的保护,代码如下:

@EnableGlobalMethodSecurity(prePostEnabled = true)

在上面的配置代码中,@EnableGlobalMethodSecurity 注解开启了方法级别的保护,括号后面的参数可选,可选的参数如下:

  • prePostEnabled:Spring Security 的 Pre 和 Post 注解是否可用, 即 @PreAuthorize 和 @PostAuthorize 是否可用
  • secureEnabled:Spring Security 的 @Secured 注解是否可用
  • jsr250Enabled:Spring Security 对 JSR-250 的注解是否可用

@PreAuthorize 注解会在进入方法前进行权限验证,@PostAuthorize 注解在方法执行后再进行权限验证,后一个注解的应用场景很少

如何在方法上写权限注解呢?例如有权限点字符串 “ROLE_ADMIN”,在方法上可以写为 @PreAuthorize(“hasRole(‘ADMIN’)”),也可以写为 @PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”),这二者是等价的。如果要加多个权限点,可以写为 @PreAuthorize(“hasAuthority(‘ADMIN’,‘USER’)”),也可以写为 @PreAuthorize(“hasAnyAuthority(‘ROLE_ADMIN’,‘ROLE_USER’)”)。

为了演示方法级别的安全保护,需要写一个 API 接口,在该接口加上权限注解。在本案例中,有一个 Blog 文章列表的 API 接口,只有管理员权限的用户才能删除 Blog,现在来实现该 API 接口。首先,需要创建 Blog 实体类,代码如下:

package com.sisyphus.entity;public class Blog {private Long id;private String name;private String content;public Blog(Long id, String name, String content) {this.id = id;this.name = name;this.content = content;}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}
}

创建 IBlogService 接口类,为了演示方便,没有 DAO 层操作数据库,而是在内存中维护一个 List<Blog> 来模拟数据库操作,包括获取所有的 Blog、根据 id 删除 Blog 的两个方法。接口类代码如下:

package com.sisyphus.service;import com.sisyphus.entity.Blog;import java.util.List;public interface IBlogService {List<Blog> getBlogs();void deleteBlog(long id);
}

IBlogService 接口类,为了演示方便,没有 DAO 层操作数据库,而是在内存中维护一个 List<Blog> 来模拟数据库操作,包括获取所有的 Blog、根据 id 删除 Blog 的两个方法。接口类代码如下:

package com.sisyphus.service;import com.sisyphus.entity.Blog;import java.util.List;public interface IBlogService {List<Blog> getBlogs();void deleteBlog(long id);
}

IBlogService 的实现类 BlogService,在构造函数方法上加入了两个 Blog 对象,并实现了 IBlogService 的两个方法。具体代码如下:

package com.sisyphus.service.impl;import com.sisyphus.entity.Blog;
import com.sisyphus.service.IBlogService;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;@Service
public class BlogService implements IBlogService {private List<Blog> list = new ArrayList<>();public BlogService(){list.add(new Blog(1L, "spring in action", "great!"));list.add(new Blog(2L, "spring boot in action", "nice!"));}@Overridepublic List<Blog> getBlogs() {return list;}@Overridepublic void deleteBlog(long id) {Iterator iterator = list.listIterator();while(iterator.hasNext()){Blog blog = (Blog) iterator.next();if (blog.getId() == id){iterator.remove();}}}
}

在 Controller 层上写两个 API 接口,一个获取所有 Blog 的列表(“/blogs”),另一个根据 id 删除 Blog(“/blogs/{id}/deletion”)。后一个 API 接口需要 “ADMIN” 的角色权限,通过注解 @PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”) 来实现。在调用删除 Blog 接口之前,会判断该用户是否具有 “ADMIN” 的角色权限。如果有权限,则可以删除;如果没有权限,则显示权限不足的界面。代码如下:

package com.sisyphus.controller;import com.sisyphus.entity.Blog;
import com.sisyphus.service.impl.BlogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;import java.util.List;@RestController
@RequestMapping("/blogs")
public class BlogController {@AutowiredBlogService blogService;@GetMappingpublic ModelAndView list(Model model){List<Blog> list = blogService.getBlogs();model.addAttribute("blogsList", list);return new ModelAndView("blogs/list", "blogModel", model);}@PreAuthorize("hasAuthority('ROLE_ADMIN')")@GetMapping("/{id}/deletion")public ModelAndView delete(@PathVariable("id") Long id, Model model){blogService.deleteBlog(id);model.addAttribute("blogsList", blogService.getBlogs());return new ModelAndView("blogs/list", "blogModel", model);}
}

我们还需要准备前端页面,代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"><body><div><table ><thead><tr><td>博客编号</td><td>博客名称</td><td>博客描述</td></tr></thead><tbody><tr th:each="blog: ${blogModel.blogsList}"><td th:text="${blog.id}"></td><td th:text="${blog.name}"></td><td th:text="${blog.content}"></td><td><div ><a th:href="@{'/blogs/' + ${blog.id}+'/deletion'}">删除</a></div></td></tr></tbody></table></div></body>
</html>

程序启动成功后,在浏览器上访问 http://localhost:8080/blogs,由于该页面受 Spring Security 保护,需要登录。使用用户名为 admin,密码为 123456 登录,该用户名对应的用户具有 “ADMIN” 的角色权限。登录成功后,页面显示 “blogs/list” 的界面

单击 “删除” 按钮,该删除按钮调用了 “/blogs/{id}/deletion” 的 API 接口。单击 “删除”,删除编号为 2 的博客,删除成功后的界面如图所示:

为了验证方法级别上的安全验证的有效性,需要用一个没有 “ADMIN” 角色权限的用户进行删除操作。用户名为 sisyphus,密码为 123456 的用户只有 “USER” 的角色权限,没有 “ADMIN” 的角色权限。该用户登录,做删除 Blog 的操作,会显示用户权限不足的界面,界面如图所示:

可见,在方法级别上的安全验证是通过相关的注解和配置来实现的。本例中的注解写在 Controller 层,如果写在 Service 层也同样生效。对 Spring Security 而言,它只控制方法,不论方法在哪个层级上

5.从数据库中读取用户的认证信息

在上述例子中,采用了从内存中配置用户信息,包括用户名、密码、用户的角色权限信息。当用户数量非常多时,这种方式显然是不可行的。本节讲述如何从数据库中读取读取用户和用户的角色权限信息。本案例中采用的数据库为 MySQL,ORM 框架为 JPA

在工程的 pom 文件上加上 MySQL 数据库连接的依赖 mysql-connector-java 和 JPA 的起步依赖 spring-boot-starter-data-jpa,代码如下:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId>
</dependency>

在工程的配置文件 application.yml 中配置数据库连接驱动、数据源、数据库用户名和密码,以及 JPA 的相关配置,配置代码如下:

server:port: 8080spring:thymeleaf:mode: HTML5encoding: UTF-8cache: falsedatasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/spring-security?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8username: rootpassword: 123456jpa:hibernate:ddl-auto: updateshow-sql: true

创建 User 实体,该类使用了 JPA 的注解 @Entity,表示该 Java 对象会被映射到数据库。id 采用的生成策略为自增加,包含了 username 和 password 两个字段,其中 authorities 为权限点的集合。具体代码如下:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;@Entity
public class User implements UserDetails, Serializable {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true)private String username;@Columnprivate String password;@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)@JoinTable(name = "user_role",joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))private List<Role> authorities;public User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setUsername(String username) {this.username = username;}public void setPassword(String password) {this.password = password;}public void setAuthorities(List<Role> authorities) {this.authorities = authorities;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return authorities;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

上面的 User 类实现了 UserDetails 接口,该接口是实现 Spring Security 认证信息的核心接口。其中,getUsername 方法为 UserDetails 接口的方法,这个方法不一定返回 username,也可以是其它的用户信息,例如手机号码、邮箱地址等。getAuthorities() 方法返回的是该用户设置的权限信息,在本例中,从数据库中读取该用户的所有角色信息,权限信息也可以是用户的其他信息,不一定是角色信息。另外需要读取密码,最后几个方法一般情况下都返回 true,也可以根据自己的需求进行业务判断。UserDetails 接口的代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.security.core.userdetails;import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();String getPassword();String getUsername();boolean isAccountNonExpired();boolean isAccountNonLocked();boolean isCredentialsNonExpired();boolean isEnabled();
}

Role 类实现了 GrantedAuthority 接口,并重写了其 getAuthority 方法。权限点可以为任何字符串,不一定是角色名的字符串,关键是 getAuthority 方法如何实现。本例的权限点是从数据库读取 Role 表的 name 字段。Role 类的代码如下:

package com.sisyphus.entity;import org.springframework.security.core.GrantedAuthority;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@Entity
public class Role implements GrantedAuthority {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false)private String name;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public void setName(String name) {this.name = name;}@Overridepublic String getAuthority() {return name;}@Overridepublic String toString() {return name;}
}

UserDao 继承了 JpaRepository,JpaRepository 默认实现了大多数单表查询的操作。UserDao 中自定义一个根据 username 获取 User 的方法,由于 JPA 已经自动实现了根据 username 字段去查找用户的方法,因此不需要额外的工作。代码如下:

package com.sisyphus.dao;import com.sisyphus.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;public interface UserDao extends JpaRepository<User, Long> {User findByUsername(String username);
}

Service 层需要实现 UserDetailsService 接口,该接口是根据用户名获取该用户的所有信息,包括用户信息和权限点,代码如下:

package com.sisyphus.service.impl;import com.sisyphus.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;@Service
public class UserService implements UserDetailsService {@Autowiredprivate UserDao userRepository;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {return userRepository.findByUsername(username);}
}

最后需要修改 Spring Security 的配置,让 Spring Security 从数据库中获取用户的认证信息,而不是之前从内存中读取,代码如下:

package com.sisyphus.config;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredUserDetailsService userDetailsService;@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/css/**", "/index").permitAll().antMatchers("/user/**").hasRole("USER").antMatchers("/blogs/**").hasRole("USER").and().formLogin().loginPage("/login").failureUrl("/login-error").and().exceptionHandling().accessDeniedPage("/401");http.logout().logoutSuccessUrl("/");}@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {//        auth
//                .inMemoryAuthentication()
//                .passwordEncoder(new BCryptPasswordEncoder())
//                .withUser("sisyphus")
//                .password(new BCryptPasswordEncoder().encode("123456"))
//                .roles("USER");
//
//        auth
//                .inMemoryAuthentication()
//                .passwordEncoder(new BCryptPasswordEncoder())
//                .withUser("admin")
//                .password(new BCryptPasswordEncoder().encode("123456"))
//                .roles("ADMIN", "USER");auth.userDetailsService(userDetailsService).passwordEncoder(new MyPasswordEncoder());}
}

这里我们使用了一个自定义密码加密类对密码进行加密,自定义加密类代码如下:

package com.sisyphus.config;import org.springframework.security.crypto.password.PasswordEncoder;public class MyPasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence charSequence) {return charSequence.toString();}@Overridepublic boolean matches(CharSequence charSequence, String s) {return s.equals(charSequence.toString());}}

在启动程序之前,需要在 MySQL 中建一个数据库,数据库名为 spring-security,建库语句如下:

CREATE DATABASE `spring-security` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci

启动程序,JPA 会连接数据库,在数据库自动建表,也可以自己在数据库中建表。数据库的表建好之后,需要在数据库库中初始化用户的信息数据,本案例的初始化用户信息的数据库脚本如下:

INSERT INTO user (id, username, password) VALUES (1, 'sisyphus', 123456);
INSERT INTO user (id, username, password) VALUES (2, 'admin', 123456);
INSERT INTO role (id, name) VALUES (1, 'ROLE_USER');
INSERT INTO role (id, name) VALUES (2, 'ROLE_ADMIN');
INSERT INTO user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 1);
INSERT INTO user_role (user_id, role_id) VALUES (2, 2);

启动程序之后,在浏览器上访问 http://localhost:8080,你会发现跟之前存在内存中的用户信息的认证效果是一样的。这说明 Spring Security 从数据库中获取了用户信息,并用作认证

四、总结

使用 Spring Security 还是比较简单的,没有想象中那么复杂。首先引入 Spring Security 相关的依赖,然后写一个配置类,该配置类继承了 WebSecurityAdapter,并在该配置类上加 @EnableWebSecurity 注解开启 Web Security。还需要配置 AuthenticationManageBuilder,AuthenticationManagerBuilder 配置了读取用户的认证信息的方式,可以从内存中读取,也可以从数据库中读取,或者用其他的方式。其次,需要配置 HttpSecurity,HttpSecurity 配置了请求的认证规则,即哪些 URI 请求需要认证、哪些不需要,以及需要拥有什么权限才能访问。最后,如果需要开启方法级别的安全配置,需要通过在配置类上加 @EnableGlobalMethodSecurity 注解开启,方法级别上的安全控制支持 secureEnabled、jsr250Enabled 和 prePostEnabled 这 3 种方式,用的最多的是 prePostEnabled。其中,prePostEnabled 包括 PreAuthorize 和 PostAuthorize 两种方式,一般只用到 PreAuthorize 这种方式

《深入理解 Spring Cloud 与微服务构建》第十六章 Spring Boot Security 详解相关推荐

  1. 《深入理解 Spring Cloud 与微服务构建》第六章 服务注册和发现 Eureka

    <深入理解 Spring Cloud 与微服务构建>第六章 服务注册和发现 Eureka 文章目录 <深入理解 Spring Cloud 与微服务构建>第六章 服务注册和发现 ...

  2. 《深入理解 Spring Cloud 与微服务构建》第三章 Spring Cloud

    <深入理解 Spring Cloud 与微服务构建>第三章 Spring Cloud 文章目录 <深入理解 Spring Cloud 与微服务构建>第三章 Spring Clo ...

  3. 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十七章 使用 Spring Cloud OAuth2 保护微服务系统 文章目录 <深入理解 Spring Cloud 与微服务构 ...

  4. 《深入理解 Spring Cloud 与微服务构建》第十三章 配置中心 Spring Cloud Config

    <深入理解 Spring Cloud 与微服务构建>第十三章 配置中心 Spring Cloud Config 文章目录 <深入理解 Spring Cloud 与微服务构建>第 ...

  5. 《深入理解 Spring Cloud 与微服务构建》第十一章 服务网关

    <深入理解 Spring Cloud 与微服务构建>第十一章 服务网关 文章目录 <深入理解 Spring Cloud 与微服务构建>第十一章 服务网关 一.服务网关简介 二. ...

  6. 《深入理解 Spring Cloud 与微服务构建》第七章 负载均衡 Ribbon

    <深入理解 Spring Cloud 与微服务构建>第七章 负载均衡 Ribbon 文章目录 <深入理解 Spring Cloud 与微服务构建>第七章 负载均衡 Ribbon ...

  7. 《深入理解 Spring Cloud 与微服务构建》第五章 Kubernetes

    <深入理解 Spring Cloud 与微服务构建>第五章 Kubernetes 文章目录 <深入理解 Spring Cloud 与微服务构建>第五章 Kubernetes 一 ...

  8. 《深入理解 Spring Cloud 与微服务构建》第四章 Dubbo

    <深入理解 Spring Cloud 与微服务构建>第四章 Dubbo 文章目录 <深入理解 Spring Cloud 与微服务构建>第四章 Dubbo 一.Dubbo 简介 ...

  9. 《深入理解Spring Cloud与微服务构建》出版啦!

    作者简介 方志朋,毕业于武汉理工大学,CSDN博客专家,专注于微服务.大数据等领域,乐于分享,爱好开源,活跃于各大开源社区.著有<史上最简单的Spring Cloud教程>,累计访问量超过 ...

  10. 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统 文章目录 <深入理解 Spring Cl ...

最新文章

  1. 匿名函数应用-多线程测试代码
  2. javascript + css 利用div的scroll属性让TAB动感十足
  3. mysql 中is not null 和 !=null的区别
  4. PropertyGrid自定义控件
  5. 一丶宝塔+青龙面板安装部署教程及命令-依赖库
  6. 流式处理框架storm浅析(下篇)
  7. springBoot+maven的打包和部署在Tomcat
  8. Linux环境:NFS--网络文件系统部署
  9. 飞机大作战游戏 1----(运用H5和Js制作)
  10. Graphicsmagick linux 中文水印乱码-new
  11. Java系列之雪花算法和原理
  12. JavaScript面试小知识
  13. mysql之 OPTIMIZE TABLE整理碎片
  14. 6-1 多态性与虚函数
  15. Linux文件夹文件创建、删除
  16. “博观而约取,厚积而薄发”——苏东坡
  17. 2159: H.ly的小迷弟
  18. 什么是UV PV DAU MAU
  19. [unity小游戏]小球运动初步制作1.0版
  20. java获取下周一_java 获取下周一日期

热门文章

  1. 10右键闪退到桌面_windows7多用户远程桌面如何设置
  2. acm之java输入输出_ACM中Java输入输出
  3. wordpress主题ajax,为自制WordPress主题/插件的后台设置页面添加ajax支持
  4. mui-scroll-wrapper mui-scroll 内容增多不出滚动条
  5. 软工实践-第二次会议
  6. 医保费用监控指标体系建立(九)其他专项分析
  7. RHEL7 -- 修改主机名
  8. JDBC学习笔记(1)
  9. 分享经验,让更多的人受益
  10. ASP.NET 程序中常用的三十三种代码(1)