第7章 SpringBoot安全管理

学习目标

  • 了解SpringSecurity安全管理的功能

  • 掌握SpringSecurity的安全配置

  • 掌握SpringSecurity自定义用户认证的实现方法

  • 掌握SpringSecurity自定义用户授权管理的实现方法

  • 掌握如何使用SpringSecurity实现页面控制

实际开发中,一些应用通常要考虑到安全性问题。例如,对于一些重要的操作,有些请求需要重要用户验明身份后才可以执行,还有一些请求需要用户具有特定权限才可以执行。这样做的意义不仅可以用来保护项目安全,还可以控制项目访问效果。

7.1 Spring Security介绍

针对项目的安全管理,Spring家族提供了安全框架Spring Security,它时一个基于Spring生态圈的,用于提供安全访问控制解决方案的框架。为了方便SpringBoot项目的安全管理,SpringBoot对Spring Security安全框架及逆行了整合支持,并提供了通用的自动化配置,从而实现了SpringSecurity安全框架中包含的多数安全管理功能。

(1)MVC Security是SpringBoot整合SpringMVC搭建web应用的安全管理框架,也是开发中使用最多的一款安全功能。

(2)WebFlux Security是SpringBoot整合Spring WebFlux搭建Web应用的安全管理。虽然Spring WebFlux框架刚出现不久,文档不够健全,但是它集成了其他安全功能的优点,后续有可能再web开发中越来越流行。

(3)OAuth2是大型项目的安全管理框架,可以实现第三方认证,单点登录等功能,但是目前SpringBoot版本还不支持OAuth2安全管理框架。

(4)Actuator Security用于对项目的一些运行环境提供安全监控,例如Health健康信息Info运行信息等,它主要作为系统指标供运维人员查看管理系统的运行情况。

上面介绍了SpringBoot整合Spring Security安全框架可以实现的一些安全管理功能。项目安全管理是一个很大的话题,开发者可以根据实际项目需求,选择性的使用Spring Security安全框架中的功能。

7.2 Spring Security快速入门

Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。其中,认证即确认用户是否登录,并对用户登录进行管控;授权即确定用户所拥有的功能权限,并对用户权限进行管控。

7.2.1 基础环境搭建

我们将会结合一个访问电影列表和详情的案例进行演示说明

(1)创建SpringBoot项目。使用Spring Initializr方式创建一个项目,再Dependencies依赖选择中选择web模块中的web依赖以及Template Engines模块中Thymeleaf依赖,然后根据提示完成项目创建。

第7章 SpringBoot安全管理_第1张图片

(2)引入页面html资源文件。再项目的resources下templates目录中,引入案例所需的资源文件。

第7章 SpringBoot安全管理_第2张图片

index.html文件时项目首页面,common和vip文件夹中分别对应的时普通用户和VIP用户可访问的页面。其中,index.html页面代码如下:




	
	影视直播厅


欢迎进入电影网站首页


该页面中通过多个超链接标签链接到多个不同的页面上,在common和VIP两个文件夹下分别放置了普通用户和VIP用户可访问的页面





影片详情


	返回
	

我不是药神

简介:根据真实社会事件改编,一位不速之客的意外到访,打破了神油店老板程勇(徐峥 饰)的平凡人生, 他从一个交不起房租的男性保健品商贩,一跃成为印度仿制药“格列宁”的独家代理商。收获巨额利润的他,生活剧烈变化, 被病患们冠以“药神”的称号。但是,一场关于救赎的拉锯战也在波涛暗涌中慢慢展开……





影片详情


	返回
	

夏洛特烦恼

简介:《夏洛特烦恼》是开心麻花2012年首度推出的话剧,由闫非和彭大魔联合编剧、执导。 2013、2014、2015年仍在上演。该作讲述了一个普通人在穿越重回到高中校园并实现了种种梦想的不可思议的经历……





影片详情


	返回
	

速度与激情

简介:《速度与激情》是罗伯·科恩等执导,于2001年至2017年范·迪塞尔、保罗·沃克(已故)、 乔丹娜·布鲁斯特、米歇尔·罗德里格兹等主演的赛车题材的动作犯罪类电影,截至2018年,一共拍了八部。之后两部续集正式定档, 《速度与激情9》和《速度与激情10》分别于2020年4月10日和2021年4月2日上映……





影片详情


	返回
	

猩球崛起

简介:《猩球崛起》是科幻片《人猿星球》的前传,由鲁伯特·瓦耶特执导,詹姆斯·弗兰科,汤姆·费尔顿, 芙蕾达·平托,布莱恩·考克斯等主演。剧情主要讲述人猿进化为高级智慧生物、进而攻占地球之前的种种际遇,主题是带有警世性质的——人类疯狂的野心所产生的恶果。 影片获得了第39届安妮奖最佳真人电影角色动画奖等奖项,同时获得了奥斯卡最佳特效奖提名,但很遗憾,并没有获奖……

(3)编写web控制层。在醒目中创建controller包,并在该包下创建一个用于页面请求处理的控制类

package com.example.demo7.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class FileController {
    //  影片详情页
    @GetMapping("/detail/{type}/{path}")
    public String toDetail(@PathVariable("type")String type, @PathVariable("path")String path) {
        return "detail/"+type+"/"+path;
    }
}

以上代码中,只编写了一个向影片详情页面请求跳转的方法toDetail(),该文件中没有设计用户登录提交以及推出操作的控制方法。

至此,我们就使用SpringBoot整合SpringMVC框架实现了一个传统且简单的web项目,目前项目没有引入任何的安全管理依赖,也没有及逆行任何安全配置。项目启动成功后,可以访问首页,单击影片进入详情页。

第7章 SpringBoot安全管理_第3张图片

第7章 SpringBoot安全管理_第4张图片

第7章 SpringBoot安全管理_第5张图片

7.2.2 开启安全管理效果测试

在项目中开启Spring Security的方式非常简单,只需要引入spring-boot-starter-security启动器即可。下面我们在项目中引入安全管理依赖,开启项目的安全管理并进行测试。

(1)添加spring-boot-starter-security启动器

在pom.xml文件中引入Spring Security安全框架的依赖启动器


        
            org.springframework.boot
            spring-boot-starter-security
        

需要说明的是,一旦项目引入以上启动器,MVC Security和WebFlux Security负责的安全功能都会立即生效(WebFlux Security生效的另一个前提是项目属于WebFlux Web项目),对于OAuth2安全管理功能来说,则还需要额外引入一些其他安全依赖。

(2)项目启动测试

启动项目后,仔细观察控制台打印输出的信息

可以看到项目启动后会在控制台自动生成一个安全密码,这个密码在每次启动项目时都时随机生成的。

通过浏览器访问“http://localhost:8080/”查看项目首页

第7章 SpringBoot安全管理_第6张图片

这时候不再进入index.html页面了,而是进入了一个登录页面,需要输入用户名和密码。这个页面并不是我们创建和编写的,而是Spring Security框架提供的安全管理登陆页面。随意输入一个用户名和密码该页面会提示用户名或密码错误。

第7章 SpringBoot安全管理_第7张图片

需要说明的是,我们在项目中加入安全管理启动器后,Security会默认提供一个可登录的用户信息,其中用户名为user,密码就是控制台随机生成的那个密码,我们在登录页面输入用户名和密码之后即可进入index.html页面。

细心的读者会发现,这种安全管理方式存在很多问题,只有一个默认的用户名,和随机生成的密码直接暴露在控制台,登录页面和错误提示页面过于简单都不是我们想要的。

7.3 MVC Security安全配置介绍

项目中添加了安全管理的项目启动器后,MVC Security安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。其中,SecurityAutoConfiguration会导入并自动配置SpringBootWebSecurityConfiguration用于启动web安全管理,UserDetailsServiceAutoConfiguration则用于配置用户身份信息。

通过自定义WebSecurityConfigurerAdapter类型的Bean组件,可以完全关闭Security提供的web应用默认安全设置,但是不会关闭UserDetailsService用户信息自动配置类。如果要关闭UserDetailsService默认用户信息配置,可以自定义UserDetailsService,AuthenticationProvider或AuthenticationManager类型的Bean组件。另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件覆盖默认访问规则。SpringBoot提供了非常多方便的方法,可用于覆盖请求映射和静态资源的访问原则。

下面我们通过Spring Security API 查看WebSecurityConfigurerAdapter的主要方法

方法 描述
configure(AuthenticationManagerBuilder auth) 定制用户认证管理器来实现用户认证
configure(HttpSecurity http) 定制基于HTTP请求的用户访问控制

7.4 自定义用户认证

通过自定义WebSecurityConfigurerAdapter类型的Bean组件,并重写configure(AuthenticationManagerBuilder auth)方法,可以自定义用户认证。针对自定义用户认证,Spring Security提供了多种自定义认证方式,包括有:In-Memory Authentication(内存身份认证),JDBC Authentication(JDBC身份认证),LDAP Authentication(LDAP身份认证),Authentication Provider(身份认证提供商)和UserDetailsService(身份详情服务)。

7.4.1 内存身份认证

In-Memory Authentication(内存身份认证)是最简单的身份认证方式,主要用于Security安全认证体验和测试。自定义内存身份认证时,只需要在重写的configure(AuthenticationManagerBuilder auth)方法中定义测试用户即可。虾米那通知SpringBoot整合Spring Security实现内存身份认证

(1)自定义WebSecurityConfigurerAdapter配置类

在项目中创建config包,在该包下创建SecurityConfig

package com.example.demo7.config;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @Classname SecurityConfig
 * @Description MVC Security管理配置的自定义WebSecurityConfigurerAdapter类
 * @Date 2019-3-5 14:52
 * @Created by CrazyStone
 */
@EnableWebSecurity  // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}

上述代码中,自定义了一个继承至WebSecurityConfigurerAdapter的SecurityConfig配置类,用于进行MVC Security自定义配置,该类上方@EnableWebSecurity

注解时一个组合注解,其效果等同于@Configuration,@Import,@EnableGlobalAuthentication的组合用法,关于这些注解的介绍具体如下:

①@Configuration注解的作用时将当前自定义的SecurityConfig类作为SpringBoot的配置类

②@Import注解的作用是根据pom.xml中导入的web模块和Security模块进行自动化配置

③@EnableGlobalAuthentication注解则用于开启自定义的全局认证

需要说明的是,如果是针对Spring WebFlux框架的安全支持,需要在项目中导入Reactive Web模块和Security模块,并使用@EnableWebFluxSecurity注解开启基于WebFlux Security的安全支持。

(2)使用内存进行身份认证

在自定义的SecurityConfig类中重写configure(AuthenticationManagerBuilder auth)方法,并在该方法中使用内存身份认证的方式进行自定义用户认证

package com.example.demo7.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

/**
 * @Classname SecurityConfig
 * @Description MVC Security管理配置的自定义WebSecurityConfigurerAdapter类
 * @Date 2019-3-5 14:52
 * @Created by CrazyStone
 */
@EnableWebSecurity  // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 用户身份认证自定义配置
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  密码需要设置编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
//        // 1、使用内存用户信息,作为测试使用
        auth.inMemoryAuthentication().passwordEncoder(encoder)
                .withUser("shitou").password(encoder.encode("123456")).roles("common")
                .and()
                .withUser("李四").password(encoder.encode("123456")).roles("vip");
}
}

以上代码中,重写了WebSecurityConfigurerAdapter类的configure(AuthenticationManagerBuilder auth)方法,并在该方法中使用内存身份认证的方式自定义了认证用户信息。定义用户认证信息时,设置了两个用户,包括用户名,密码和角色。

在进行自定义用户认证时,需要注意以下几个问题:

① 从Spring Security5开始,自定义用户认证必须设置密码编码器用于保护密码,否则控制台会出现“IIIegalArgumentException:There is no PasswordEncoder mapped for the id null” 异常错误。

② Spring Security提供了多种密码编码器,包括BcryptPasswordEncoder,Pbkdf2PasswordEncoder,ScryptPasswordEncoder等,密码设置不限于本例中的BcryptPasswordEncoder密码编码器。

③ 自定义用户认证时,可以定义用户角色roles,也可以定义用户权限authorities。在进行赋值时,权限通常是在角色值的基础上添加“ROLE_”前缀。例如,authorities("ROLE_common")和roles("common")是等效的。

④ 自定义用户认证时,可以为某个用户一次指定多个角色或权限,例如roles("common","vip")或者authorities("ROLE_common","ROLE_vip")。

(3)效果测试

重新运行项目,仔细查看控制台打印信息,发现没有默认安全管理随机生成的密码了。通过浏览器访问“http://localhost:8080/”查看首页,浏览器自动跳转到“http://localhost:8080/login”地址访问登录页面

第7章 SpringBoot安全管理_第8张图片

当用户输入错误的用户名和密码会显示用户名或密码错误,当用户输入正确的用户名和密码则进入index.html页面,这里我们设定了两个用户名和密码可以进行登录访问匹配。

实际开发中,用户都是在页面注册和登录时进行认证管理的,而非在程序内部使用内存管理的方式手动控制注册用户,所以上述使用内存身份认证的方式无法用于实际生产,只可以作为初学者的测试使用。

7.4.2 JDBC身份认证

JDBC Authentication(JDBC 身份认证)时通过JDBC连接数据库对已有用户身份进行认证

(1)数据准备

JDBC身份认证的本质是使用数据库中已有的用户信息在项目中实现用户认证服务,所以需要提前准备好相关数据。这里我们使用之前按创建的名为springbootdata的数据库,在该数据库中创建三张表,并添加几条模拟数据。

#选择使用数据库
USE springbootdata;
#创建表t_customer并插入相关数据
DROP TABLE IF EXISTS t_customer;
CREATE TABLE t_customer(
id INT(20) NOT NULL AUTO_INCREMENT,
username VARCHAR(200) DEFAULT NULL,
PASSWORD VARCHAR(200) DEFAULT NULL,
valid TINYINT(1) NOT NULL DEFAULT '1',
PRIMARY KEY (id))ENGINE=INNODB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO t_customer VALUES('1','shitou','$2a$10$GoES9wT/h40EnuizD2sbXeI6HunciK35hfd7g4udRCAfqFNIh0Lbi','1'),
('2','李四','$2a$10$GoES9wT/h40EnuizD2sbXeI6HunciK35hfd7g4udRCAfqFNIh0Lbi','1');
#创建表t_authority并插入相关数据
DROP TABLE IF EXISTS t_authority;
CREATE TABLE t_authority(
id INT(20) NOT NULL AUTO_INCREMENT,
authority VARCHAR(20) DEFAULT NULL,
PRIMARY KEY (id))ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO t_authority VALUES('1','ROLE_common'),('2','ROLE_vip');
#创建表 t_customer_authority并插入相关数据
DROP TABLE IF EXISTS t_customer_authority;
CREATE TABLE t_customer_authority(
id INT(20) NOT NULL AUTO_INCREMENT,
customer_id INT(20) DEFAULT NULL,
authority_id INT(20) DEFAULT NULL,
PRIMARY KEY (id))ENGINE=INNODB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
INSERT INTO t_customer_authority VALUES('1','1','1'),('2','2','2');

(2)添加JDBC连接数据库的依赖启动器

打开项目的pom.xml文件,在该文件中添加MySQL数据库连接驱动的依赖和JDBC连接依赖


        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
        
            mysql
            mysql-connector-java
            8.0.11
        

(3)进行数据库连接配置

在项目的全局配置文件application.properties中编写对应的数据库连接配置

#MySQL数据库连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=admin

(4)使用JDBC进行身份认证

在configure(AuthenticationManagerBuilder auth)方法中使用JDBC进行身份认证

package com.example.demo7.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

import javax.sql.DataSource;

/**
 * @Classname SecurityConfig
 * @Description MVC Security管理配置的自定义WebSecurityConfigurerAdapter类
 * @Date 2019-3-5 14:52
 * @Created by CrazyStone
 */
@EnableWebSecurity  // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    /**
     * 用户身份认证自定义配置
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  密码需要设置编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		System.out.println(encoder.encode("123456"));
//        // 2、使用JDBC进行身份认证
        String userSQL ="select username,password,valid from t_customer " +
                "where username = ?";
        String authoritySQL="select c.username,a.authority from t_customer c,t_authority a,"+
                "t_customer_authority ca where ca.customer_id=c.id " +
                "and ca.authority_id=a.id and c.username =?";
        auth.jdbcAuthentication().passwordEncoder(encoder)
                .dataSource(dataSource)
                .usersByUsernameQuery(userSQL)
                .authoritiesByUsernameQuery(authoritySQL);
        
    }
}

运行项目,通过浏览器访问的流程与上一节是一样的,输入的用户名和密码必须要与数据库中存储的用户名和密码一致,不然还是会显示用户名或密码错误。

7.4.3 UserDetailsService身份认证

对于用户流量较大的项目来说,频繁的使用JDBC进行数据库查询认证不仅麻烦,而且会降低网站响应速度。对于一个完善的项目来说,如果某些业务已经实现了用户信息查询的服务,就没必要使用JDBC进行身份认证了。

下面假设当前项目中已经有用户信息查询的业务方法,这里,在已有的用户信息服务的基础上选择使用UserDetailsService进行自定义用户身份认证。

在项目的pom.xml文件中添加jpa依赖启动器


    org.springframework.boot
    spring-boot-starter-data-jpa

再添加缓存启动器依赖

 
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

(1)定义查询用户及角色信息的服务接口

为了案例演示,在项目中创建一个service包,包中创建一个CustomerService业务处理类,用来通过用户名查询用户信息及权限信息

package com.example.demo7.service;


import com.example.demo7.domain.Authority;
import com.example.demo7.domain.Customer;
import com.example.demo7.repository.AuthorityRepository;
import com.example.demo7.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;

/**
 * @Classname CustomerService
 * @Description 对用户数据结合Redis缓存进行业务处理
 * @Date 2019-3-5 15:58
 * @Created by CrazyStone
 */
@Service
public class CustomerService {
    @Autowired
    private CustomerRepository customerRepository;
    @Autowired
    private AuthorityRepository authorityRepository;
    @Autowired
    private RedisTemplate redisTemplate;

    // 业务控制:使用唯一用户名查询用户信息
    public Customer getCustomer(String username){
        Customer customer=null;
        Object o = redisTemplate.opsForValue().get("customer_"+username);
        if(o!=null){
            customer=(Customer)o;
        }else {
            customer = customerRepository.findByUsername(username);
            if(customer!=null){
                redisTemplate.opsForValue().set("customer_"+username,customer);
            }
        }
        return customer;
    }
    // 业务控制:使用唯一用户名查询用户权限
    public List getCustomerAuthority(String username){
        List authorities=null;
        Object o = redisTemplate.opsForValue().get("authorities_"+username);
        if(o!=null){
            authorities=(List)o;
        }else {
            authorities=authorityRepository.findAuthoritiesByUsername(username);
            if(authorities.size()>0){
                redisTemplate.opsForValue().set("authorities_"+username,authorities);
            }
        }
        return authorities;
    }
}

在项目中创建repository包,在包中创建CustomerRepository和AuthorityRepository类

package com.example.demo7.repository;


import com.example.demo7.domain.Authority;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;


/**
 * Created by admin on 2018-11-19.
 */
public interface AuthorityRepository extends JpaRepository {
    @Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)
    public List findAuthoritiesByUsername(String username);

}
package com.example.demo7.repository;


import com.example.demo7.domain.Customer;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by crazyStone on 2018-11-06.
 */
public interface CustomerRepository extends JpaRepository {
    Customer findByUsername(String username);
}

在项目中创建domain包,在包中创建Customer和Authority实体类

package com.example.demo7.domain;


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * Created by crazyStone 2018-11-7.
 */
@Entity(name = "t_customer")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String username;
    private String password;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password=" + password +
                '}';
    }
}
package com.example.demo7.domain;


import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * Created by admin on 2018-11-20.
 */
@Entity(name = "t_authority ")
public class Authority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String authority ;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getAuthority() {
        return authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public String toString() {
        return "Authority{" +
                "id=" + id +
                ", authority='" + authority + '\'' +
                '}';
    }
}

以上代码中,CustomerService是项目中存在的Customer业务处理类,该类结合Redis缓存定义了通过username获取用户信息和用户权限信息的方法。

(2)定义UserDetailsService用于封装认证用户信息

UserDetailsService是Security提供的用于封装认证用户信息的接口,该接口提供的loadUserByUsername(String s)方法用于通过用户名加载用户信息。使用UserDetailsService进行身份认证时,自定义一个UserDetailsService接口的实现类,通过loadUserByUsername(String s)方法封装用户详情信息并返回UserDetails对象供Security认证使用。

在service包中创建一个自定义实现类

package com.example.demo7.service;
import com.example.demo7.domain.Authority;
import com.example.demo7.domain.Customer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @Classname UserDetailsServiceImpl
 * @Description 自定义一个UserDetailsService接口实现类进行用户认证信息封装
 * @Date 2019-3-5 16:08
 * @Created by CrazyStone
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private CustomerService customerService;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 通过业务方法获取用户及权限信息
        Customer customer = customerService.getCustomer(s);
        List authorities = customerService.getCustomerAuthority(s);
        // 对用户权限进行封装
        List list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
        // 返回封装的UserDetails用户详情类
        if(customer!=null){
            UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);
            return userDetails;
        } else {
            // 如果查询的用户不存在(用户名不存在),必须抛出此异常
            throw new UsernameNotFoundException("当前用户不存在!");
        }
    }
}

需要注意的是,CustomerService业务处理类获取User实体类时,必须对当前用户进行非空判断,这里使用throw进行异常处理,如果查询的用户为空,throw会抛出UsernameNotFoundException的异常。如果没有使用throw异常处理,Security将无法识别,导致程序整体报错。

(3)使用UserDetailsService进行身份认证

接下来我们在configure(AuthenticationManagerBuilder auth)方法中使用UserDetailsService身份认证的方式进行自定义用户认证

package com.example.demo7.config;

import com.example.demo7.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;

import javax.sql.DataSource;

/**
 * @Classname SecurityConfig
 * @Description MVC Security管理配置的自定义WebSecurityConfigurerAdapter类
 * @Date 2019-3-5 14:52
 * @Created by CrazyStone
 */
@EnableWebSecurity  // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    /**
     * 用户身份认证自定义配置
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //  密码需要设置编码器
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();


        // 3、使用UserDetailsService进行身份认证
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
    }
}

先通过@Autowired注解引入了自定义的UserDetailsService接口实现类UserDetailsServiceImpl,然后再重写了configure(AuthenticationManagerBuilder auth)方法中使用UserDetailsService身份认证的方式自定义了认证用户信息。再使用UserDetailsService身份认证时,可直接调用userDetailsService(T userDetailsService)对UserDetailsServiceImpl实现类进行认证,认证过程中需要对密码进行编码设置。

(4)效果测试

运行项目,项目启动后,通过浏览器访问服务器根地址,这里需要开启redis服务器,最后访问效果与前面两种效果一致。

访问的时候发现报错,通过错误代码发现Customer实体类需要进行redis序列化,因此需要对redis缓存进行序列化,在config包中创建RedisConfig类

package com.example.demo7.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
 * Created by shitou on 2018-11-22.
 * Spring Boot 2.X版本自定义序列化方式
 */
@Configuration
public class RedisConfig {
    /**
     *  定制Redis API模板RedisTemplate
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        // 使用JSON格式序列化对象,对缓存数据key和value进行转换
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 设置RedisTemplate模板API的序列化方式为JSON
        template.setDefaultSerializer(jacksonSeial);
        return template;
    }

    /**
     * 定制Redis缓存管理器RedisCacheManager,实现自定义序列化并设置缓存时效
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
        RedisSerializer strSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
        // 解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jacksonSeial.setObjectMapper(om);
        // 定制缓存数据序列化方式及时效
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofDays(1))   // 设置缓存有效期为1天
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial))
                .disableCachingNullValues();   // 对空数据不进行缓存
        RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
        return cacheManager;
    }
}

再次运行,则正常了,打开redis缓存查看信息

第7章 SpringBoot安全管理_第9张图片

至此,关于SpringBoot整合Spring Security中的自定义用户认证讲解完毕。内存身份认证最为简单,主要用作测试和新手体验;JDBC身份认证和UserDetailsService身份认证在实际开发中使用较多,而这两种认证方式的选择,主要根据实际开发中已有业务的支持来确定。

7.5 自定义用户授权管理

当一个系统建立之后,通常需要适当的做一些权限控制,使得不同用户具有不同的权限操作系统。例如,一般的项目中都会做一些简单的登录控制,只有特定用户才能登录访问。

7.5.1 自定义用户访问控制

实际生产中,网站访问多是基于HTTP请求的,在前面章节及逆行MVC Security安全配置介绍时,我们已经分析出通过重写WebSecurityConfigAdapter类的config(HttpSecurty http)方法可以对基于HTTP的请求访问进行控制。

config(HttpSecurty http)方法的参数类型是HttpSecurity类,HttpSecurity类提供了Http请求的限制以及权限,Seesion管理配置,CSRF跨站请求问题等方法

方法 描述
authorizeRequests() 开启基于HttpServletRequest请求访问的控制
formLogin() 开启基于表单的用户登录
httpBasic() 开启基于HTTP请求的Basic认证登录
logout() 开启退出登录的支持
sessionManagement() 开启Session管理配置
rememberMe() 开启记住我功能
csrf() 配置CSRF跨站请求伪造防护功能

此处重点讲解用户访问控制,这里先对authorizeRequests()方法的返回值进行查看

方法 描述
antMatchers(String...antPatterns) 开启Ant风格的路径匹配
mvcMatchers(String...patterns) 开启MVC风格的路径匹配
regexMatchers(String...regexPatterns) 开启正则表达式的路径匹配
and() 功能连接符
anyRequest() 匹配任何请求
rememberMe() 开启记住我功能
access(String attribute) 匹配给定的SpEL表达式计算结果是否为true
hasAnyRole(String...roles) 匹配用户是否有参数中的任意角色
hasRole(String role) 匹配用户是否有某一角色
hasAnyAuthority(String...authorities) 匹配用户是否有参数中的任意权限
hasAuthority(String authority) 匹配用户是否有某一权限
authenticated() 匹配已经登录认证的用户
fullyAuthenticated() 匹配完整登录认证的用户(非rememberMe登录用户)
haslpAddress(String ipaddressExpression) 匹配某IP地址的访问请求
permitAll() 无条件请求进行放行

以上列举的是用户请求访问的常用方法,如果想了解更多方法可以通过查看API文档。

(1)自定义用户访问控制

打开之前创建的MVC Security自定义配置类SecurityConfig,重写configure(HttpSecurity http)方法进行用户访问控制

/**
     * 用户授权管理自定义配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户授权管理
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                // 需要对static文件夹下静态资源进行统一放行
                .antMatchers("/login/**").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }

以上代码中,config()方法设置了用户访问权限,其中,路径为“/”的请求直接放行;路径为“/detail/common/* *”的请求,只有用户角色为common(即ROLE_common权限)才允许访问;路径为“/detail/vip/** ”的请求,只有用户角色是cip(即ROLE_vip权限)才允许访问;其他请求则要求用户必须先进行登录认证。

(2)效果测试

运行项目,项目启动成功后,通过浏览器访问服务器根路径,因为自定义用户访问控制中对“/”的请求是直接放行的,因此会直接进入首页界面,说明自定义用户访问控制生效。

第7章 SpringBoot安全管理_第10张图片

在项目首页单击普通电影或者VIP电影名称查询电影详情,则需要进行登录,进入登录页面

第7章 SpringBoot安全管理_第11张图片

登录之后可以看到详细页面

第7章 SpringBoot安全管理_第12张图片

当点击返回按钮时,会再次回到首页,之前登录的普通用户还处于登录状态,再次点击VIP专享电影名称则不能查看

第7章 SpringBoot安全管理_第13张图片

表示当前登录的用户没有权限查看vip电影详情,说明配置的用户访问控制对不同的请求拦截也生效了。另外,当前示例没有配置完善的用户注销功能,所以登录一个用户后要切换其他用户的化将浏览器重启,再次使用新账号登录即可。(因为该示例是在UserDetailsService身份认证知识点上进行编写的,因此切换新账号登录时,一定要确定缓存中是否已经保存了要登录的新账号

7.5.2 自定义用户登录

通过钱买你几个示例演示可以发现,项目中并没有配置用户登录页面和登录处理方式,但是演示过程中却提供了一个默认的用户登录页面,并且进行了自动登录处理,这就是Spring Security提供的默认登录处理机制。实际开发中,通常要求定制更美观的用户登录也米娜,并配置有更好的错误提示信息,此时需要自定义用户登录控制。下面我们就围绕formLogin()这个方法来探索并讲解自定义用户登录的具体实现。

方法 描述
loginPage(String loginPage) 用户登录页面跳转路径,默认为get请求的/login
successForwardUrl(String forwardUrl) 用户登录成功后的重定向地址
successHandler(AuthenticationSuccessHandler successHandler) 用户登录成功后的处理
defaultSuccessUrl(String defaultSuccessUrl ) 用户直接登录后默认跳转地址
failureForwardUrl(String forwardUrl) 用户登录失败后的重定向地址
failureUrl(String authenticationFailureUrl) 用户登录失败后的跳转地址,默认为/login?error
failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 用户登录失败后的错误处理
usernameParameter(String usernameParameter) 登录用户的用户名参数,默认为username
passwordParameter(String passwordParament) 登录用户的密码参数,默认为password
loginProcessingUrl(String loginProcessingUrl) 登录表单提交的路径,默认为post请求的/login
permitAll() 无条件对请求进行放行

了解了Spring Security中关于用户登录的相关方法后,下面我们在前一个自定义用户访问控制案例的基础上实现自定义用户登录功能

(1)自定义用户登录页面

要实现自定义用户登录功能,首先必须根据需要自定义一个用户登录页面。在项目的resources/templates目录下新创建一个名为login的文件夹(专门处理用户登录),在该文件夹中创建一个用户登录页面login.html

第7章 SpringBoot安全管理_第14张图片




    
    用户登录界面
    
    


    

上述代码中,通过form标签定义了一个用户登录共功能,且登录数据以post方式通过“/userLogin”路径进行提交。其中,登录表单中的用户名参数和密码参数可以自行定义,登录数据提交方式必须为post,提交的路径也可以自行定义。同时在代码中有一个专门用来存储登录错误后返回错误信息的div标签,在该div标签中使用th:if="${param.error}"来判断请求中是否带有一个error参数,从而判断是否登录成功,该参数时Security默认的,用户可以自行定义。

上图项目中还需要引入两个CSS文件和两个图片文件,用来渲染用户登录页面,它们都存在于/login/**目录下,需要提前引入这些静态资源文件到static目录中。

(2)自定义用户登录跳转

在之前创建的FileController中添加以下代码,用于跳转到登录页面login.html

// 向用户登录页面跳转
@GetMapping("/userLogin")
public String toLoginPage() {
    return "login/login";
}

在上述代码中,配置了请求路径为"/userLogin"的GET请求,并向静态资源根目录下的login文件夹下的login.html页面跳转。

Spring Security默认采用GET方式的“/login”请求用于向登录页面跳转,使用POST方式的“/login”请求用于对登录后的数据处理。

Spring Security默认向登录页面跳转时,采用的请求方式时GET,请求路径时“/login”,如果要处理登录后的数据,默认采用的请求方式是POST,请求路径是“/login”

(3)自定义用户登录控制

完成上面的准备工作后,打开SecurityConfig类,实现用户登录控制

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                // 需要对static文件夹下静态资源进行统一放行
                .antMatchers("/login/**").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated();

//        // 自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name").passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");
      }

①loginPage("/userLogin")方法指定了向自定义登录页面跳转的请求路径(前面定义的toLoginPage()方法的请求映射路径),并使用permit All()方法对进行登录跳转的请求进行放行

②usernameParameter("name")和passwordParameter("pwd")方法用来接收登录时提交的用户名和密码。这里的参数name和pwd必须和login.html中用户名,密码中的name属性值保持一致,如果login.html中的name属性值时默认的username和password,这两个方法就可以省略。

③defaultSuccessUrl("/")方法指定了用户登录成功后默认跳转到项目首页

④failureUrl("/userLogin?error")方法用来控制用户登录认证失败后的跳转路径,该方法默认参数为“/userLogin?error”。其中,参数中的“/userLogin”为向登录页面跳转的映射,error是一个错误标识,组哟给是登录失败后在登录页面进行接收判断,例如,login.html示例中的${param.error},这两者必须保持一致

⑤antMatchers("/login/**").permitAll()方法的作用是对项目static文件夹下login包及其子包中的静态资源文件进行统一进行放行处理。如果没有对静态资源放行,未登录的用户访问项目首页时就无法加载页面关联的静态资源文件。

(4)效果测试

运行项目,通过浏览器访问服务器根路径,进入到首页后,点击普通电影或者vip电影,进入登录页面才能查看详情,这个时候进入的登录页面是我们自己编写的页面

第7章 SpringBoot安全管理_第15张图片

输入redis缓存中保存的正确的用户名和密码即可登录后,查看详情电影信息,登录后回到首页,查看其他电影详情时,还会被拦截要求切换账号登录

第7章 SpringBoot安全管理_第16张图片

当输入的用户名或密码错误时,则会显示我们自己设置的错误提示信息

第7章 SpringBoot安全管理_第17张图片

7.5.3 自定义用户退出

自定义用户退出主要考虑退出后的会话如何管理以及跳转到那个页面。HttpSecurity类的logout()方法用来处理用户退出,它默认处理路径为“/logout”的post类型请求,同时也会清除Session和“Remember Me”(记住我)等任何默认用户配置。

logout()方法中涉及用户退出的主要方法及说明

方法 描述
logoutUrl(String logoutUrl) 用户退出处理控制URL,默认为post请求的/logout
logoutSuccessUrl(String logoutSuccessUrl) 用户退出成功后的重定向地址
logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) 用户退出成功后的处理器设置
deleteCookies(String...cookieNamesToClear) 用户退出后删除指定Cookie
invalidateHttpSession(boolean invalidateHttpSession) 用户退出后是否立即清除Session(默认为true)
clearAuthentication(boolean clearAuthentication) 用户退出后是否立即清除Authentication用户认证信息(默认为true)

(1)添加自定义用户退出链接

要实现自定义用户退出功能,必须先在某个页面定义用户退出链接或者按钮。为了简化操作,我们在之前创建的项目首页上方新增一个用户退出链接

上述代码中,新增了一个form标签进行注销控制,且定义的退出表单action为“/mylogout”(默认为“/logout”),方法为post。

需要说明的时,SpringBoot项目中引入SpringSecurity框架后会自定开启CSRF防护功能(跨站请求伪造防护),用户退出时必须使用POST请求,如果关闭了CSRF防护功能,那么可以使用任意方式的HTTP请求进行用户注销。

(2)自定义用户退出控制

在页面中定义好用户退出链接后,不需要再Controller控制层中额外定义用户退出方法,可以直接再Security中定制logout()方法实现用户退出。打开SecurityConfig类,重写config(HttpSecurity http)方法进行用户退出控制

/**
     * 用户授权管理自定义配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户授权管理
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                // 需要对static文件夹下静态资源进行统一放行
                .antMatchers("/login/**").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated();

//        // 自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name").passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");
//
//        // 自定义用户退出控制
        http.logout()
                .logoutUrl("/mylogout")
                .logoutSuccessUrl("/");
        }

上述代码中,使用logout()及相关方法实现了用户退出功能。其中,logoutUrl("/mylogout")方法指定了用户退出的请求路径,这个路径与index.html页面退出表单中action的值必须保持一致,如果退出表单使用了默认的“/logout”请求,则此方法可以省略,logoutSuccessUrl("/")方法指定了用户退出成功后重定向到“/”地址(即再次重定向到项目首页)。再用户退出后,用户会话信息则会默认清除,此情况下无须手动配置。

(3)效果测试

运行项目,访问服务器根地址,进入首页

第7章 SpringBoot安全管理_第18张图片

查看电影详情信息,进行正确的登录后,进入电影详情信息页面,点击返回按钮,回到首页,这个时候用户还在登录状态,需要点击注销进行退出用户登录。

第7章 SpringBoot安全管理_第19张图片

注销退出登录状态后,再次查看电影详情,则需要重新登录才能访问。

7.5.4 登录用户信息获取

再传统项目中进行用户登录处理时,通常会查询用户是否存在,如果存在则登录成功,同时将当前用户放再Session中。前面的案例中,使用整合Security进行用户授权管理后并没有显示登录后的用用户处理情况,那么这种情况下登录后的用户存放在哪里呢?存储的用户数据及结构时怎样的?

(1)使用HttpSession获取用户信息

在之前创建的FileController控制类中新增一个用于获取当前会话用户信息的方法

/**
 * 通过传统的HttpSession获取Security控制的登录用户信息
 * @param session
 */
@GetMapping("/getuserBySession")
@ResponseBody
public void getUser(HttpSession session) {
    // 从当前HttpSession获取绑定到此会话的所有对象的名称
    Enumeration names = session.getAttributeNames();
    while (names.hasMoreElements()){
        // 获取HttpSession中会话名称
        String element = names.nextElement();
        // 获取HttpSession中的应用上下文
        SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
        System.out.println("element: "+element);
        System.out.println("attribute: "+attribute);
        // 获取用户相关信息
        Authentication authentication = attribute.getAuthentication();
        UserDetails principal = (UserDetails)authentication.getPrincipal();
        System.out.println(principal);
        System.out.println("username: "+principal.getUsername());
    }
}

上述代码中,在getUser(HttpSession session)方法中通过获取当前HttpSession的相关方法遍历并获取了会话中的用户信息。其中,通过getAttribute(element)获取会话对象时,默认返回的时一个Object对象,其本质是一个SecurityContextImpl类,为了方便查看对象数据,所以强制转换为SecurityContextImpl,在获取认证用户信息时,使用了Authentication的getPrincipal()方法,默认返回的也是一个Object对象,其本质是封装用户信息的UserDetails封装类,其中包括有用户名,密码,权限,是否过期等。

以debug模式重启项目及逆行效果测试,项目启动成功后,通过浏览器访问服务器根地址,随意查看一个影片详情进行用户登录,登录成功后,在保证当前浏览器未关闭的情况下,使用同一浏览器执行“http://localhost:8080/getuserBySession”来获取用户详情,通过浏览器审查元素可以看出,当前HttpSession会话中只有一个key为“SPRING_SECURITY_CONTEXT”的用户信息,并且用户信息被封装在SecurityContextImpl类对象中。通过SecurityContextImpl类的相关方法可以进一步获取到当前登录用户的更多信息,其中关于用户的主要信息(例如用户名,用户权限等)都封装在UserDetails类中。

(2)使用SecurityContextHolder获取用户信息

SpringSecurity针对拦截的登录用户专门提供了一个SecurityContextHolder类,来获取SpringSecurity的应用上下文SecurityContext,进而获取封装的用户信息。

在FileController控制类中新增一个getUser2()方法

/**
 * 通过Security提供的SecurityContextHolder获取登录用户信息
 */
@GetMapping("/getuserByContext")
@ResponseBody
public void getUser2() {
    // 获取应用上下文
    SecurityContext context = SecurityContextHolder.getContext();
    System.out.println("userDetails: "+context);
    // 获取用户相关信息
    Authentication authentication = context.getAuthentication();
    UserDetails principal = (UserDetails)authentication.getPrincipal();
    System.out.println(principal);
    System.out.println("username: "+principal.getUsername());
}

上述代码中,通过Security提供了SecurityContextHolder类先获取了应用上下文对象SecurityContext,并通过其相关方法获取了当前登录用户信息。通过与HttpSession方法获取用户信息的示例对比可以发现,这两种方法的区别就是获取SecurityContext的不同,其他后续方法基本一致。

至此,关于SpringBoot整合SpringSecurity拦截后的登录用户信息获取就已经讲解完毕了。这里介绍的两种方法中,HttpSession的方式获取用户信息相对比较传统,而且必须引入HttpSession对象,而Security提供的SecurityContextHolder则相对简便,也是在Security项目中相对推荐的使用方式。

7.5.5 记住我功能

在实际开发中,有些项目为了用户登录方便还会提供记住我(Remember-Me)功能。如果用户登录时勾选了记住我选项,那么在一段有效时间内,会默认自动登录,并允许访问相关页面,这就免去了重复登录操作的麻烦。SpringSecurity提供了用户登录控制的同时,当然也提供了对应的记住我功能,HttpSecurity类的主要方法rememberMe()就是SpringSecurity用来处理记住我功能的。

方法 描述
rememberMeParameter(String rememberMeParameter) 指示在登录时记住用户的HTTP参数
key(String key) 记住我认证生成的Token令牌标识
tokenValiditySeconds(int tokenValiditySeconds) 记住我Token令牌有效期,单位为s(秒)
tokenRepository(PersistentTokenRepository tokenRepository) 指定要使用PersistentTokenRepository,用来配置持久化Token令牌
alwaysRemember(boolean alwaysRemember) 是否应该始终创建记住我Cookie,默认为false
clearAuthentication(boolean clearAuthentication) 是否设置Cookie为安全的,如果设置为true,则必须通过HTTPS进行连接请求

需要说明的是,SpringSecurity针对记住我功能提供了两种实现:一种是简单的使用加密来保证基于Cookie中Token的安全,另一种是通过数据库或其他持久化机制来保存生成的Token。

(1)基于简单加密Token的方式

基于简单加密Token的方式实现记住我功能非常简单,当用户选择记住我并成功登录后,SpringScurity将会生成一个Cookie并发送给客户端浏览器。其中,Cookie值由下列方式组合加密而成。

base64(username+":"+expirationTime+":"+md5Hex(username+":"+expirationTime+":"+password+":"+key));

上述Cookie值的生成方式中,username代表登录的用户名,password代表登录用户密码,expirationTime标识记住我中Token的失效日期,以毫秒为单位,key表示放置修改Token的标识。

基于简单加密Token的方式中的Token在指定的时间内有效,且必须保证Token中所包含的username,password和key没有被改变。需要注意的是,这种加密方式其实是存在安全隐患的,任何人获取到该记住我功能的Token后,都可以在该Token过期之前进行自动登录,只有当用户觉察到Token被盗用后,才会对自己的登录密码进行修改来立即使其原有的记住我Token失效。

我们已经在自定义登录页面上添加了一个多选按钮记住我的勾选标签,记住我勾选框的name属性值设为了“rememberme”,而Security提供的记住我功能的name属性值默认为“remember-me”,打开SecurityConfig类进行配置记住我功能的代码

 /**
     * 用户授权管理自定义配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户授权管理
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                // 需要对static文件夹下静态资源进行统一放行
                .antMatchers("/login/**").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated();

//        // 自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name").passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");
//
//        // 自定义用户退出控制
        http.logout()
                .logoutUrl("/mylogout")
                .logoutSuccessUrl("/");
//
//        // 定制Remember-me记住我功能
        http.rememberMe()
                .rememberMeParameter("rememberme")
                .tokenValiditySeconds(200);

    }

上述代码中,rememberMeParameter("rememberme")方法指定了记住我勾选框的name属性值,如果页面中使用了默认“remember-me”,则该方法可以省略,tokenValiditySeconds(200)方法设置了记住我功能中Token的有效期为200s。

重新运行项目,在查看影片详情信息的时候,登录页面中勾选记住我勾选框进行登录,在规定的时候内不需要重新登录认证。

(2)基于持久化Token的方式

持久化Token的方式与简单加密Token的方式在实现Remember-Me功能上大体相同,都是在用户选择【记住我】并成功登录后,将生成的Token存入Cookie中并发送到客户端浏览器,在下次用户通过同一客户端访问系统时,系统将直接从客户端Cookie中读取Token进行认证。两者的主要区别在于:基于简单加密Token的方式,生成的Token将在客户端保存一段时间,如果用户不退出登录,或者不修改密码,那么在Cookie失效之前,任何人都可以无限制的使用该Token进行自动登录,而基于持久化Token的方式采用如下实现逻辑。

1.用户选择【记住我】成功登录后,Security会把username,随机产生的序列号,生成的Token进行持久化存储(例如一个数据表中),同时将它们的组合生成一个Cookie发送给客户端浏览器。

2.当用户再次访问系统时,首先检查客户端携带的Cookie,如果对应Cookie中包含的username,序列号和Token于数据库中保存的一致,则通过验证并自动登录,同时系统将重新生成一个新的Token替换数据库中旧的Token,并将新的Cookie再次发送给客户端。

3.如果Cookie中的Token不匹配,则很有可能时用户的Cookie被盗用了。由于盗用者使用初次生成的Token进行登录时会生成一个新的Token,所以当用户在不知情时再次登录就会出现Token不匹配的情况,这时就需要重新登录,并生成新的Token和Cookie。同时SpringSecurity就可以发现Cookie可能被盗用的情况,它将删除数据库中于当前用户相关的所有Token记录,这样盗用者使用原有的Cookie将不能再次登录。

4.如果用户访问系统时没有携带Cookie,或者包含的username和序列号于数据库中保存的不一致,那么将会引导用户登录页面。

从以上实现逻辑可以看出,持久化Token的方式比简单加密Token的方式相对更加安全。使用简单加密Token的方式,一旦用户的Cookie被盗用,在Token有效期内,盗用者可以无限制的自动登录进行恶意操作,直到用户本人发现并修改密码才会避免这种问题,而使用持久化Token的方式相对安全,用户每登录一次都会生成新的Token和Cookie,但也给盗用者留下了在用户进行第2次登录前进行恶意操作的机会,只有在用户进行第2次登录并更新Token和Cookie时,才会避免这种问题。因此,总体来讲,对于安全性要求很高的应用,不推荐使用Remember-Me功能。

下面我们结合前面介绍的rememberMe()相关方法来实现这种持久化Token方式记住我功能。为了对持久化Token进行存储,需要在数据库中创建一个存储Cookie信息的持续登录用户表persistent_logins(这里仍在之前创建的springbootdata数据库中创建该表)

create table persistent_logins(username varchar(64) not null,
								series varchar(64) primary key,
                              token varchar(64) not null,
                              last_used timestamp not null);

上述sql语句中创建了一个名为persistent_logins的数据表,其中username存储用户名,series存储随机生成的序列号,token存储每次访问更新的Token,last_used表示最近登录日期。需要说明的时,在默认情况下基于持久化Token的方式会使用上述官方提供的用户表persistent_logins进行持久化Token的管理,读者不需要自定义存储Cookie信息的用户表。

在完成存储Cookie信息的用户表创建以及页面记住我功能勾选框设置后,打开SecurityConfig类

    /**
     * 用户授权管理自定义配置
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 自定义用户授权管理
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                // 需要对static文件夹下静态资源进行统一放行
                .antMatchers("/login/**").permitAll()
                .antMatchers("/detail/common/**").hasRole("common")
                .antMatchers("/detail/vip/**").hasRole("vip")
                .anyRequest().authenticated();

//        // 自定义用户登录控制
        http.formLogin()
                .loginPage("/userLogin").permitAll()
                .usernameParameter("name").passwordParameter("pwd")
                .defaultSuccessUrl("/")
                .failureUrl("/userLogin?error");
//
//        // 自定义用户退出控制
        http.logout()
                .logoutUrl("/mylogout")
                .logoutSuccessUrl("/");
//
//        // 定制Remember-me记住我功能
        http.rememberMe()
                .rememberMeParameter("rememberme")
                .tokenValiditySeconds(200)
                // 对cookie信息进行持久化管理
                .tokenRepository(tokenRepository());

    }

    /**
     * 持久化Token存储
     * @return
     */
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository(){
        JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
        jr.setDataSource(dataSource);
        return jr;
    }

上述代码中,与基于简单加密的Token方式相比,在持久化Token方式的rememberMe()示例中加入了tokenRepository(tokenRepository())方法对Cookie信息进行持久化管理。其中的tokenRepository()参数会返回一个设置dataSource数据源的JdbcTokenRepositoryImpl实现类对象,该对象包含操作Token的各种方法。

运行项目,通过浏览器访问服务器根地址,进行登录访问详情信息,观察数据库是否保存记住我功能的用户登录信息,在Token有效期内进行多次登录,数据库中的token会自动更新,当用户注销时,数据库中的数据会自动被删除,如果用户在Token有效期失效之后退出,再次登录时,数据库表中会新增一条信息,而不会在原记录上进行更新。

第7章 SpringBoot安全管理_第20张图片

7.5.6 CSRF防护功能

CSRF(跨站请求伪造),也被称为一键攻击或者会话控制,通常缩写为CSRF或XSRF,是一种对网站的恶意利用。与传统的XSS攻击(跨站脚本攻击)相比,CSRF攻击更加难以防范,被认为比XSS更具危险性。CSRF攻击可以在受喊着毫不知情的情况下以受害者的名义伪造请求发送给攻击页面,从而在用户未授权的情况下执行在权限保护之下的操作。

例如,一个用户Tom登录银行站点服务器准备进行转账操作,在此用户信息有效期内,Tom被诱导查看了一个黑客恶意网站,该网站就会获取到Tom登录后的浏览器与银行网站之间尚未过期的Seesion信息,而Tom浏览器的Cookie中含有Tom银行账户的认证信息,此时黑客就会伪装成Tom认证后的合法用户对银行账户进行非法操作。

在讨论如何抵御CSRF攻击之前,先要明确CSRF攻击的对象,也就是要保护的对象。从上面的例子可知,CSRF攻击时黑客借助受害者的Cookie骗取服务器的新人,但是黑客并不能获取Cookie,也看不到Cookie的具体内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客无法进行解析。黑客所能做的就是伪造正常用户给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。因此,针对CSRF攻击要保护的对象是那些可以直接产生数据变化的服务,而对于读取数据的服务,可以不进行CSRF保护。例如,银行转账操作会改变账号金额,需要进行CSRF保护。获取银行卡等级信息是读取操作,不会改变数据,可以不需要保护。

在业界目前防御CSRF攻击主要由以下3中策略:

1.验证HTTP Referer字段。

2.在请求地址中添加Token并验证

3.在HTTP头中自定义

SpringSecurity安全框架提供了CSRF防御相关方法

方法 描述
disable() 关闭Security默认开启的CSRF防御功能
csrfTokenRepository(CsrfTokenRepositor csrfTokenRepositor ) 指定要使用的CsrfTokenRepositroy(Token令牌持久化仓库)。默认是由LazyCsrfTokenRepository包装的HttpSessionCsrfTokenRepository
requireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) 指定针对什么类型的请求应用CSRF防护功能。默认设置是忽略GET,HEAD,TRACE和OPTIONS请求,而处理并防御其他所有请求

1.CSRF防护功能关闭

SpringBoot整合SpringSecurity默认开启了CSRF防御功能,并要求数据修改的请求方法都需要经过Security配置的安全认证后方可正常访问,否则无法正常发送请求。

(1)创建数据修改页面。在templates文件夹下创建csrf文件夹,在该文件夹下创建html文件




    
    用户修改


用户名:
密  码:

上述代码中,编写了一个表单页面,并使用post请求将修改的信息提交给“/updateUser”地址

(2)编写后台控制层方法。在controller包下创建一个用于CSRF页面请求测试的控制类CSRFController

package com.example.demo7.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * @Classname CSRFController
 * @Description TODO
 * @Date 2019-3-6 15:59
 * @Created by CrazyStone
 */
@Controller
public class CSRFController {
    // 向用户修改页跳转
    @GetMapping("/toUpdate")
    public String toUpdate() {
        return "csrf/csrfTest";
    }
    // 用户修改提交处理
    @ResponseBody
    @PostMapping(value = "/updateUser")
    public String updateUser(@RequestParam String username, @RequestParam String password,
                             HttpServletRequest request) {
        System.out.println(username);
        System.out.println(password);
        String csrf_token = request.getParameter("_csrf");
        System.out.println(csrf_token);
        return "ok";
    }
}

编写的toUpdate() 方法用于向用户修改页面跳转,updateUser()方法用于对用户修改提交数据处理。其中,在updateUser()方法中知识演示了获取的请求参数,没有具体是业务实现。

(3)CSRF默认防护效果测试。运行项目,通过浏览器访问“http://localhost:8080/toUpdate”打开修改用户信息页面,由于前面配置了请求拦截,该请求会被拦截跳转到登录页面,通过正确的用户名和密码登录后才能进行修改页面,输入的用户名和密码错误则会显示报错提示页面。

随意输入修改后的用户名和密码,如果产生403错误和Forbidden(禁止)的错误提示信息,而后台也没有任何响应。这说明整合使用的SpringSecurity安全框架默认启用了CSRF安全防护功能,而上述示例被拦截的本质原因就是数据修改请求中没有携带CSRF Token相关的参数信息,所以被认为是不安全的请求,如果选择关闭Security默认开启的CSRF防御功能的话,配置非常简单,在SecurityConfig类的configure方法中加上以下代码

// 可以关闭Spring Security默认开启的CSRF防护功能
http.csrf().disable();

上述代码中展示了关闭CSRF防御功能的配置方式,其他代码示例无须变动。需要说明的是,这种直接关闭CSRF防御的方式简单粗暴,不太推荐使用,如果强行关闭后网站可能会面临CSRF攻击的危险。

SpringScurity针对不同类型的数据修改请求提供了不同方式的CSRF Token配置,主要包括由:针对Form表单数据修改的CSRF Token配置和针对Ajax数据修改请求的CSRF Token配置。

2.针对Form表单数据修改的CSRF Token配置

针对Form表单类型的数据修改请求,Security支持在Form表单中提供一个携带CSRF Token信息的隐藏域,与其他修改数据一起提交,这样后台就可以获取并验证该请求是否为安全

第7章 SpringBoot安全管理_第21张图片

上述代码中,Form表单中的input隐藏标签携带了Security提供的CSRF Token信息。其中,th:name="${csrf.parameterName}"会获取Security默认提供的CSRF Token对应的值__csrf, th:value="${csrf.token}"会获取Security默认随机生成的CSRF Token对应的value值。在表单中添加上述配置后,无须其他配置就可以正常实现数据修改请求,后台配置的Security会自动获取识别请求中的CSRF Token信息并进行用户信息验证,从而判断是否安全。

需要说明的是,针对Thymeleaf模板页面中的表单数据修改请求,除了可以使用上述示例方式显示配置CSRF Token信息提交数据修改请求外,还可以使用Thymeleaf模板的th:action属性配置CSRF Token信息




    
    用户修改


用户名:
密  码:

上述代码中,使用th:action属性配置了表单数据修改后的请求路径,而在表单中并没有提供CSRF Token信息的隐藏域,但仍然可以正常的执行数据修改请求。这是因为使用th:action属性配置请求时,会默认携带CSRF Token信息,无须开发者手动添加,这也解释了在前面编写的login.html页面进行用户登录时为何可以正常执行的原因。

7.6 Security管理前端页面

前面讲解了通过SpringScurity对后台增加了权限控制,前端页面并没有做任何处理,前端页面显示的还是对应的连接等内容,用户体验较差。

1.添加thymeleaf-extras-springsecurity5依赖启动器

在项目pom.xml中添加thymeleaf-extras-springsecurity5依赖启动器



    org.thymeleaf.extras
    thymeleaf-extras-springsecurity5

需要注意是,上述添加的依赖启动器,其版本号同样是由SpringBoot统一整合并管理的。

2.修改前端页面,使用Security相关标签进行页面控制

打开项目首页,引入Security安全标签

第7章 SpringBoot安全管理_第22张图片




	
	影视直播厅


欢迎进入电影网站首页

游客您好,如果想查看电影请登录

您好,您的用户权限为,您有权观看以下电影


 

sec:authorize="isAnonymous()"属性判断用户是否未登录,只有匿名用户(未登录用户)才会显示“请登录”连接提示

sec:authorize="isAuthenticated()"属性来判断用户是否已登录,只有认证用户(登录用户)才会显示登录用户信息和注销链接等提示

sec:authorize="hasRole('common')"属性定义了只有角色未common(对应权限Authority为ROLE_common)且登录的用户才会显示普通电影列表信息

sec:authorize="hasAuthority('ROLE_vip')"属性定义了只有权限为ROLE_vip且登录的用户才会显示VIP电影列表信息。

sec:authentication="name"和sec:authentication="principal.authorities"用于显示登录用户名和权限

第7章 SpringBoot安全管理_第23张图片

本章小结

本章主要讲解了SpringBoot的MVCSecurity安全管理。首先介绍了SpringSecurity安全框架以及SpringBoot支持的安全管理,并体验了SpringBoot默认的安全管理,讲解了SpringSecurity自定义用户认证以及授权管理,介绍了Security与前端的整合实现页面安全管理控制。希望大家通过本章的学习,能够掌握SpringBoot的安全管理机制,并灵活运用在实际开发中,提升项目的安全性。

习题

一、填空题

  1. SpringBoot整合SpringSecurity安全框架实现的安全管理功能有()、WebFluxSecurity、OAuth2、Actuator Security。

  2. Security默认提供一个可登录的用户信息,用户名为user,密码为()。

  3. 重写WebSecurityConfigurerAdapter类的()方法可以自定义用户认证。

  4. 重写WebSecurityConfigurerAdapter类的()方法可以对基于HTTP的请求访问进行控制。

  5. 自定义WebSecurityConfigurerAdapter类上的()注解用于开启基于WebFluxSecurity的安全支持。

二、判断题

  1. 项目中引入spring-boot-starter-security依赖后,还需要使用@EnableSecurity开启安全管理支持。()

  2. WebSecurityConfigurerAdapter类的configure(HttpSecurity http)方法用于构建认证管理器。()

  3. 初始化权限表数据时,权限值必须带有“ROLE_”前缀。()

  4. 定义JDBC身份认证时,定义权限查询的SQL语句必须返回用户名,密码和权限几个字段信息。()

  5. 定义UserDetailsService身份认证时,如果用户为空,需要抛出UserNotFoundException异常。()

三、选择题

  1. SpringSecurity提供了多种自定义认证方式,包括有()。(多选)

    A.JDBC Authentication

    B.LDAP Authentication

    C.AuthenticationProvider

    D.UserDetailsService

  2. 下列关于使用JDBC身份认证方式创建用户/权限表以及初始化数据性说法,错误的是()。

    A.用户表的用户名username必须唯一

    B.创建用户表时,必须额外定义一个tinyint类型的字段

    C.初始化用户表数据时,插入的用户密码必须时对应编码器编码后的密码

    D.初始化角色表数据时,角色值必须带有“ROLE_”前缀

  3. 下列关于configure(HttpSecurity http)方法中参数HttpSecurity 类的说法,正确的是()。

    A.authorizeRequest()方法开启基于HttpServletRequest请求访问的限制

    B.formLogin()方法开启基于表单的用户登录

    C.rememberMe()方法开启记住我功能

    D.csrf()方法配置CSRF跨站请求伪造防护功能

  4. 下列关于自定义用户登录中的相关说法,错误的是()。

    A.loginPage(String loginPage)指定用户登录页面跳转路径,默认为GET请求的/login

    B.failureUrl(String authenticationFailureUrl )指定用户登录失败后的跳转地址,默认为/login?failure

    C.loginProcessingUrl(String loginProcessingUrl)指定登录表单提交的路径,默认为POST请求的/login

    D.项目加入Security后,可以不对static文件下的静态资源文件进行统一放行处理

  5. 下列关于使用Security整合Thymeleaf实现页面的管理的说法,错误的是()。

    A.SpringBoot2.1.3版本中添加thymeleaf-extras-springsecurity5依赖不需要手动指定版本号

    B.sec:authorize="!isAuthenticated()“用于判断用户没有认证

    C.sec:authorize="hasAuthority('ROLE_vip')"用于哦按段用户是否有ROLE_vip权限

    D.sec:authentication="name"属性用于显示登录用户名name

 

你可能感兴趣的:(Spring,Boot,spring,boot,安全,java)