15.3.1 构建Spring Boot Security工程

使用IDEA的Sping Initializr方式建一个Spring Boot工程。创建完成后,在工程的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>com.forezp</groupId>
<artifactId>springboot-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot-security</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</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>
</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>

15.3.2 配置Spring Security

1.配置WebSecurityConfigurerAdapter

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

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser
("forezp").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
}

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

(1)应用的每一个请求都需要认证。

(2)自动生成了一个登录表单。

(3)可以用username和password来进行认证。

(4)用户可以注销。

(5)阻止了CSRF攻击。

(6)Session Fixation保护。

(7)安全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

(8)集成了以下的Servlet API的方法。

  • HttpServletRequest#getRemoteUser()
  • HttpServletRequest.html#getUserPrincipal()
  • HttpServletRequest.html#isUserInRole(java.lang.String)
  • HttpServletRequest.html#login(java.lang.String, java.lang.String)
  • HttpServletRequest.html#logout()

2.配置HttpSecurity

WebSecurityConfigurerAdapter配置了如何验证用户信息。那么Spring Security如何知道是否所有的用户都需要身份验证呢?又如何知道要支持基于表单的身份验证呢?工程的哪些资源需要验证,哪些资源不需要验证?这时就需要配置HttpSecurity。

新建一个SecurityConfig 类继承WebSecurityConfigurerAdapter类作为HttpSecurity的配置类,通过复写configure(HttpSecurity http)方法来配置HttpSecurity。本案例的配置代码如下:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected 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("/");
}
...
}

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

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

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

@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 accesssDenied() {
return "401";
}
}

15.3.3 编写相关界面

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

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

spring:
thymeleaf:
mode: HTML5
encoding: UTF-8
cache: false

登录界面(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角色用户: forezp / 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界面,如图15-1所示。

Spring Boot Security案例详解_css

▲图15-1 localhost:8080/index界面

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

Spring Boot Security案例详解_html_02

▲图15-2 登录界面

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

Spring Boot Security案例详解_spring_03

▲图15-3 界面http://localhost:8080/user/index

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

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
…//省略代码
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 在内存中存放用户信息
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("forezp").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser
("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN");
return manager;
}
}

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

Spring Boot Security案例详解_css_04

▲图15-4 “ADMIN”角色没有权限访问/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”角色权限的用户访问。

15.3.4 Spring Security方法级别上的保护

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

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

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

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

一般来说,只会用到prePostEnabled。因为@PreAuthorize注解和@PostAuthorize 注解更适合方法级别的安全控制,并且支持Spring EL表达式,适合Spring开发者。其中,@PreAuthorize 注解会在进入方法前进行权限验证,@PostAuthorize 注解在方法执行后再进行权限验证,后一个注解的应用场景很少。

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

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

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的两个方法。接口类代码如下:

public interface IBlogService {
List<Blog> getBlogs();
void deleteBlog(long id);
}

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

@Service
public class BlogService implements IBlogService {
private List<Blog> list=new ArrayList<>();
public BlogService(){
list.add(new Blog(1L, " spring in action", "good!"));
list.add(new Blog(2L,"spring boot in action", "nice!"));
}

@Override
public List<Blog> getBlogs() {
return list;
}

@Override
public void deleteBlog(long id) {
Iterator iter = list.iterator();
while(iter.hasNext()) {
Blog blog= (Blog) iter.next();
if (blog.getId()==id){
iter.remove();
}
}
}
}

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

@RestController
@RequestMapping("/blogs")
public class BlogController {

@Autowired
BlogService blogService;
@GetMapping
public 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(value = "/{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);
}
}

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

Spring Boot Security案例详解_微服务架构_05

▲图15-5 /blogs/list网页

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

Spring Boot Security案例详解_微服务架构_06

▲图15-6 /blogs/list界面删除博客编号为2后的界面

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

Spring Boot Security案例详解_html_07

Spring Boot Security案例详解_html_08

▲图15-7 删除博客权限不足

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


Spring Boot Security案例详解_css_09

方志朋 著

  • Springcloud微服务项目实战,springcloud入门教程
  • 微服务架构设计模式教程
  • Java架构师书籍,架构整洁之道,架构修炼之道

作为Java语言的落地微服务框架,Spring Cloud已经在各大企业普遍应用,各大云厂商也支持Spring Cloud微服务框架的云产品,因此熟练掌握Spring Cloud是面试者的加分项,《深入理解Spring Cloud与微服务构建 第2版》的十八章内容全面涵盖了通过Spring Cloud构建微服务的相关知识点,并且在第一版的基础上针对Spring Cloud的新功能做了全新改版。

1.基于Greenwich版本,全面讲解Spring Cloud原生组件。

2.深入原理,辅以图解,生动串联整个Spring Cloud生态。

3.总结提升,利用综合案例展现构建微服务系统的全过程。

4.附带全书源码供,读者可到异步社区本书页面下载,方便学习和使用。