Spring Boot 技术探索

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

35、Spring Boot之Spring Security

平台环境:

名称

版本号

Mac OS X

10.15.2

JDK

1.8.0_201

Apache Maven

3.6.0

IntelliJ IDEA

2019.3 (Ultimate Edition)

Spring Boot

2.2.2.RELEASE

 

  什么是Spring Security?

  Spring Security是一个功能强大、高度可自定义的身份验证和访问控制框架。事实上,它是所有基于Spring的应用程序的安全标准。

 

DEMO

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</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-security</artifactId>
    </dependency>


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

 

application.properties

spring.thymeleaf.cache=false


# security
# 如果用代码方式配置了账号密码与权限,则这里的配置失效
spring.security.user.name=admin
spring.security.user.password=123456

 

创建Thymeleaf文件

路径:src/main/resources/templates/

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
    <p><a th:href="@{/login}" target="_blank">登录</a></p>
    <h1>Hello!</h1>
    <p>Welcome to CodelabX!</p>
    <p>进入<a th:href="@{/content}">content页面</a>(仅限ADMIN与USER权限)</p>
    <p>进入<a th:href="@{/admin}">admin页面</a>(仅限ADMIN权限)</p>
    <p>进入<a th:href="@{/edit}">edit页面</a>(仅限ADMIN权限)</p>
    <p>进入<a th:href="@{/update}">update路径</a>(仅限ADMIN权限)</p>
</body>
</html>

 

content.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>content</title>
</head>
<body>
    <form method="post" action="/logout">
        <a th:href="@{/}">返回首页</a>
        <button  type="submit">退出登录</button>
    </form>
    <h1>content页面,例如个人资料设置(仅限登录后访问)</h1>
    <p>XXXXX:XXXXXXXXXXXXXXXX</p>
    <p>XXXXX:XXXXXXXXXXXXXXXX</p>
</body>
</html>

 

admin.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>admin</title>
</head>
<body>
    <form method="post" action="/logout">
        <a th:href="@{/}">返回首页</a>
        <button type="submit">退出登录</button>
    </form>
    <h1>admin</h1>
    <p>管理员页面</p>
</body>
</html>

 

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
    <div th:if="${param.error}">
        用户名或密码错
    </div>
    <div th:if="${param.logout}">
        您已注销成功
    </div>
    <a th:href="@{/}">返回首页</a>
    <form th:action="@{/login}" method="post">
        <div><label> 用户名 : <input type="text" name="username"/> </label></div>
        <div><label> 密 码 : <input type="password" name="password"/> </label></div>
        <div><input type="submit" value="登录"/></div>
    </form>
</body>
</html>

 

edit.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>admin</title>
</head>
<body>
    <form method="post" action="/logout">
        <a th:href="@{/}">返回首页</a>
        <button  type="submit">退出登录</button>
    </form>
    <h1>edit</h1>
    <p>编辑页面</p>
</body>
</html>

 

update.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>admin</title>
</head>
<body>
    <form method="post" action="/logout">
        <a th:href="@{/}">返回首页</a>
        <button type="submit">退出登录</button>
    </form>
    <h1>update</h1>
    <p>更新页面</p>
</body>
</html>

 

SecurityConfig

package com.example.demo.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.crypto.bcrypt.BCryptPasswordEncoder;


@Configuration
@EnableWebSecurity // 启用Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests()
                .antMatchers("/").permitAll() // 配置不验证权限的目录
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')")
                .anyRequest().authenticated() // 其他的请求需要验证权限
                .and().formLogin() // 设置登录页面(如果不设置.loginPage(),将使用默认登录页面)
                .loginPage("/login") // 使用自定义登录页面(如果注释掉此行,将使用默认登录页面)
                .permitAll()
                .and().logout().permitAll() // 允许访问退出登录路径/logout
                .and().csrf().ignoringAntMatchers("/logout"); // 忽略CSRF保护的路径
    }


    // 代码方式配置账号密码与权限
    // 如果application.properties和这里同时设置了账号密码与权限,则以这里设置的为准
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder()) // 设置密码加密方式
                .withUser("user") // 设置用户名
                .password(new BCryptPasswordEncoder().encode("888888")) // 设置密码(需与前边设置的加密方式一致)
                .roles("USER") // 设置权限角色
                .and()
                .withUser("admin")
                .password(new BCryptPasswordEncoder().encode("123456"))
                .roles("ADMIN", "USER");
    }
}

 

SecurityController

package com.example.demo.controller;


import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;


@Controller
public class SecurityController
{
    @RequestMapping("/")
    public String index()
    {
        return "index";
    }


    @RequestMapping("/content")
    public String content()
    {
        return "content";
    }


    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login()
    {
        return "login";
    }


    @RequestMapping("/admin")
    public String admin()
    {
        return "admin";
    }


    // 仅限ADMIN权限执行此方法
    // @PostAuthorize("hasAuthority('ADMIN')") 先执行方法,后验证权限,这个很少用
    // 注意在配置类要加上启用标签
    // @EnableGlobalMethodSecurity(prePostEnabled = true)
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @RequestMapping("/edit")
    public String adminPath()
    {
        return "edit";
    }


    // 注意在配置类要加上启用标签
    // @EnableGlobalMethodSecurity(securedEnabled = true)
    @Secured({"ROLE_ADMIN"})
    @RequestMapping("/update")
    public String update()
    {
        return "update";
    }
}

 

什么是CSRF攻击?

CSRF全拼为Cross Site Request Forgery,译为跨站请求伪造。了解CSRF攻击的最好方法是看一个具体的例子。

 

假设您的银行网站提供了一个表单,允许从当前登录的用户向另一个银行帐户转账。例如,转账表单可能如下所示:

Transfer form

<form method="post" action="/transfer">
    <input type="text" name="amount"/>
    <input type="text" name="routingNumber"/>
    <input type="text" name="account"/>
    <input type="submit" value="Transfer"/>
</form>

 

相应的HTTP请求可能如下所示:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded


amount=100.00&routingNumber=1234&account=9876

 

现在假设你通过了银行网站的权限认证,然后,在不注销的情况下访问了一个邪恶的网站。邪恶网站包含一个HTML页面,其格式如下:

Evil transfer form

<form method="post" action="https://bank.example.com/transfer">
    <input type="hidden" ame="amount" value="100.00"/>
    <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
    <input type="hidden" name="account" value="evilsAccountNumber"/>
    <input type="submit" value="Win Money!"/>
</form>

 

你喜欢赢钱,所以你点击了Win Money!按钮。在此过程中,您无意中向恶意用户转了100美元。发生这种情况的原因是,虽然邪恶网站看不到您的cookies,但与您的银行网站关联的cookies仍会随着请求一同发送过去。

 

更糟糕的是,整个过程可以使用JavaScript自动完成。这意味着你甚至不需要点击按钮。此外,当访问一个受到XSS攻击的可信站点时,这种情况也很容易发生。那么,我们如何保护我们的用户免受此类攻击呢?

 

保护免受CSRF攻击

CSRF攻击之所以可行的原因是来自受害者网站的HTTP请求和来自攻击者网站的请求完全相同。这意味着无法既拒绝来自攻击者网站的请求,又允许来自银行网站的请求。为了防止CSRF攻击,我们需要确保请求中存在攻击者站点无法提供的内容,以便我们可以区分这两个请求。

 

Spring提供了两种防止CSRF攻击的机制:

 

权限控制方式列表:

方法名

解释

access(String)

Spring EL 表达式结果为 true 时可访问

anonymous()

匿名可访问

denyAll()

用户不可以访问

fullyAuthenticated()

用户完全认证可访问(非 remember me 下自动登录)

hasAnyAuthority(String...)

参数中任意权限的用户可访问

hasAnyRole(String...)

参数中任意角色的用户可访问

hasAuthority(String)

某一权限的用户可访问

hasRole(String)

某一角色的用户可访问

permitAll()

所有用户可访问

rememberMe()

允许通过 remember me 登录的用户访问

authenticated()

用户登录后可访问

hasIpAddress(String)

用户来自参数中的 IP 时可访问

这里需要注意的是hasRole()和access()虽然都可以给角色赋予权限,但有所区别,比如hasRole()修饰的角色“/admin/**”,那么拥有ADMIN权限的用户访问地址xxx/admin和xxx/admin/*均可,如果使用access()修饰的角色,那么访问地址xxx/admin权限受限,请求xxx/admin/可以通过。

 

 

参考文档

https://docs.spring.io/spring-security/site/docs/5.2.2.BUILD-SNAPSHOT/reference/htmlsingle/#csrf-explained

 

 

 

Bootstrap Thumbnail Second
MySQL

MySQL is the world's most popular open source database.

GO

Bootstrap Thumbnail Third
算法基础

本书介绍了什么是计算机算法,如何描述它们,以及如何来评估它们。

GO