Spring Boot Security案例详解

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所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2GNHC4G-1571393034954)(https://old.epubit.com/api/storage/getbykey/original?key=1909b06af753e7d5ca19)]

▲图15-2 登录界面

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-27oXoOQ0-1571393034961)(https://old.epubit.com/api/storage/getbykey/original?key=190914d5e822858d578c)]

▲图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.附带全书源码供,读者可到异步社区本书页面下载,方便学习和使用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值