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所示。
▲图15-1 localhost:8080/index界面
单击上面界面的“去/user/index保护的界面”文字,由于“/user/index”界面需要“USER”权限,但还没有登录,会被重定向到登录界面“/login.html”,登录界面如图15-2所示。
▲图15-2 登录界面
这时,用具有“USER”角色的用户登录,即用户名为forezp,密码为123456。登录成功,界面会被重定向到http://localhost:8080/user/index界面,注意该界面是具有“USER”角色的用户才具有访问权限。界面显示如图15-3所示。
▲图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所示。
▲图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所示。
▲图15-5 /blogs/list网页
单击“删除”按钮,该删除按钮调用了“/blogs/{id}/deletion”的API接口。单击“删除”,删除编号为2的博客,删除成功后的界面如图15-6所示。
▲图15-6 /blogs/list界面删除博客编号为2后的界面
为了验证方法级别上的安全验证的有效性,需要用一个没有“ADMIN”角色权限的用户进行删除操作。用户名为forezp,密码为123456的用户只有“USER”的角色权限,没有“ADMIN”的角色权限。用该用户登录,做删除Blog的操作,会显示用户权限不足的界面,界面如图15-7所示。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20191018180711490.png
▲图15-7 删除博客权限不足
可见,在方法级别上的安全验证是通过相关的注解和配置来实现的。本例中的注解写在Controller层,如果写在Service层也同样生效。对Spring Security而言,它只控制方法,不论方法在哪个层级上。
本文截选自《深入理解Spring Cloud与微服务构建》(第2版)
方志朋 著
- 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.附带全书源码供,读者可到异步社区本书页面下载,方便学习和使用。