SpringSecurity

SpringSecurity

一、简介

1.1 概要

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

Spring Security是一个功能强大的身份验证和访问控制框架,用于保护Java应用程序中的资源。它提供了一套全面的安全性解决方案,可以用于Web应用程序、REST API、微服务和其他类型的Java应用程序。

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication(哦翻疼肯神))和用户授权Authorization(哦佛ruai任神))两个部分,这两点也是 Spring Security 重要核心功能。

Spring Security是基于Spring框架的模块,可以与Spring框架无缝集成。它提供了一组易于使用的API和配置选项,使开发人员能够快速、灵活地实现应用程序的安全性需求。

总之,Spring Security是一个功能强大且灵活的安全框架,可以帮助开发人员保护应用程序的资源,并提供多种身份验证和授权机制,以及防止常见的安全攻击。

我们先来看看 Spring Security 。其官方对自己介绍如下:

 Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
 ​
  Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于Spring的应用程序的事实标准。
  
 Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirementsSpring
 ​
  Security是一个专注于为Java应用程序提供身份验证和授权的框架。与所有Spring项目一样,Spring Security的真正威力在于它可以多么容易地扩展以满足定制需求

一般Web应用的需要进行认证和授权。

认证(Authentication):验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权(Authorization):经过认证后判断当前用户是否有权限进行某个操作

而认证和授权就是SpringSecurity作为安全框架的核心功能。

1.2 历史

Spring Security的历史可以追溯2003年,当时它是一个独立的开源项目,名为Acegi Security。Acegi Security最初由Ben Alex(爱我吃)创建,旨在为Spring框架提供安全性功能。

Acegi Security在其初期的版本中提供了基本的身份验证和访问控制功能。随着时间的推移,Acegi Security逐渐发展壮大,并在Spring社区中获得了广泛的认可和采用。

SpringSource是Spring框架的创始公司

2007年,SpringSource(搜四)(现在的VMware)收购了Acegi Security,并将其整合到Spring框架中。随后,Acegi Security更名为Spring Security,成为Spring框架的官方安全模块。

自那时以来,Spring Security不断发展和演进,逐步成为Java应用程序中最受欢迎和广泛使用的安全框架之一。它持续提供新的功能和改进,以适应不断变化的安全需求和技术发展。

Spring Security的发展历程中,它从最初的基本身份验证和访问控制功能逐步扩展到支持更多的身份验证机制、授权策略、单点登录、记住我功能等。它还提供了与其他安全相关的标准和协议(如OAuth、OpenID Connect)的集成。

今天,Spring Security已经成为保护Spring应用程序的首选安全框架,并在许多企业和开源项目中得到广泛应用。它的强大功能、灵活性和可扩展性使开发人员能够轻松地实现各种安全需求,并保护应用程序的资源免受潜在的安全威胁。

问题:

  1. 什么是Spring Security?它的主要目的是什么?

1.3 Security、Shiro 选择问题

1.3.1 spring Security

spring 技术栈的组成部分

除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高,毕竟Spring Security是Spring家族的。

一般流程为:
  1. 当用户登录时,前端将用户输入的用户名、密码信息传输到后台,后台用一个类对象将其封装起来,通常使用的是UsernamePasswordAuthenticationToken这个类。

  2. 程序负责验证这个类对象。验证方法是调用Service根据username从数据库中取用户信息到实体类的实例中,比较两者的密码,如果密码正确就成功登陆,同时把包含着用户的用户名、密码、所具有的权限等信息的类对象放到SecurityContextHolder(安全上下文容器,类似Session)中去。

  3. 用户访问一个资源的时候,首先判断是否是受限资源。如果是的话还要判断当前是否未登录,没有的话就跳到登录页面。

  4. 如果用户已经登录,访问一个受限资源的时候,程序要根据url去数据库中取出该资源所对应的所有可以访问的角色,然后拿着当前用户的所有角色一一对比,判断用户是否可以访问(这里就是和权限相关)。

优点

spring-security对spring整合较好,使用起来更加方便; 有更强大的spring社区进行支持; 支持第三方的 oauth 授权,官方网站:spring-security-oauth

特点:
  • 和 Spring 无缝整合。

  • 全面的权限控制。

  • 专门为 Web 开发而设计。

  • 旧版本不能脱离 Web 环境使用。

  • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独

  • 引入核心模块就可以脱离 Web 环境。

  • 重量级

1.3.2 Apache Shiro

Shiro是一个强大而灵活的开源安全框架,能够处理认证、授权、会话管理和密码加密。它具有易于理解的Java Security API,简单的身份认证和授权,支持多种数据源,内置的会话管理和简单的加密API。Shiro不依赖于任何框架或容器,可以独立运行。

Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。

Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。

如下是它所具有的特点:

  • 简单易用

  • 轻量级

  • 易于理解的 Java Security API;

  • 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);

  • 对角色的简单的签权(访问控制),支持细粒度的签权;

  • 支持一级缓存,以提升应用程序的性能;

  • 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;

  • 非常简单的加密 API;

  • 不跟任何的框架或者容器捆绑,可以独立运行。

简单来说

Apache 旗下的轻量级权限控制框架。特点:

  • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。

  • 通用性。

    • 好处:不局限于 Web 环境,可以脱离 Web 环境使用。

    • 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。

Shiro四大核心功能:Authentication,Authorization,Cryptography,Session Management

四大核心功能介绍:

Authentication:用于验证用户的身份是否合法,支持多种认证方式和自定义配置。

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;用于保护用户密码的安全,支持常见的加密算法和灵活的配置。

Cryptography (库润帕克非):加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Shiro架构

Shiro三个核心组件:Subject, SecurityManager 和 Realms.

Subject (撒播宅特) :Subject是Shiro的核心概念,代表当前正在与应用程序交互的用户。Subject可以是一个人、一个设备或者一个第三方系统。Subject封装了用户的身份信息和相关的安全操作,如登录、注测、权限检查等。Subject可以通过SecurityManager进行身份认证和授权,并与应用程序进行交互。

SecurityManager (买嫩着):相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

Realm (外姆斯) :域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

Realm主要用于身份认证和授权的功能

shiro的优点

shiro的代码更易于阅读,且使用更加简单; shiro可以用于非web环境,不跟任何框架或容器绑定,独立运行;

shiro的缺点

授权第三方登录需要手动实现;

总结

区别

  1. 成熟度:Apache Shiro是一个独立的项目,从2004年开始开发,经过多年的发展和改进,已经相对成熟稳定。而Spring Security是Spring框架的一部分,从2004年开始作为Spring Security项目进行开发,与Spring框架紧密集成。

  2. 学习曲线:Apache Shiro相对来说更加简单易用,它提供了清晰的API和简洁的概念,可以更快地上手。而Spring Security更加复杂,因为它是基于Spring框架的,需要熟悉Spring框架的概念和配置。

  3. 功能扩展:Apache Shiro提供了灵活的配置选项和可插拔的组件,可以根据需求进行定制和*扩展。而Spring Security集成了Spring框架的强大特性,可以与Spring的其他模块(如Spring BOOT)无缝集成,提供更多的功能和扩展性。

优缺点: Apache Shiro的优点:

  • 简单易用,学习曲线低。

  • 轻量级,对应用程序的性能影响较小。

  • 灵活性高,提供了可插拔的组件和灵活的配置选项。

Apache Shiro的缺点:

  • 社区相对较小,可能在某些特定功能或问题上缺乏支持。

  • 功能相对较少,相对于Spring Security来说,可能缺乏一些高级功能。

Spring Security的优点:

  • 与Spring框架紧密集成,可以无缝使用Spring的其他模块。

  • 功能丰富,提供了许多高级特性和扩展点。

  • 强大的社区支持,有大量的文档、教程和解决方案。

Spring Security的缺点:

  • 学习曲线较陡峭,相对复杂。

  • 对于小型应用程序而言,可能过于重量级,对性能有一定影响。

二、认证 和 授权

1.1 什么是认证

现在这个移动互联网时代,大家每天都在刷手机,常用的软件有微信、支付宝、头条等,下边拿微信来举例子说明认证相关的基本概念,在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码登录微信的过程就是认证。

系统为什么要认证?

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信

息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手

机短信登录,指纹认证等方式。

1.2 什么是授权

拿微信来举例子,微信登录成功后用户即可使用微信的功能,比如,发红包、发朋友圈、添加好友等,没有绑定

银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,发红包功能、发朋友圈功能都是微信的资源即

功能资源,用户拥有发红包功能的权限才可以正常使用发送红包功能,拥有发朋友圈功能的权限才可以使用发朋友

圈功能,这个根据用户的权限来控制用户使用资源的过程就是授权。

为什么要授权

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,

控制不同的用户能够访问不同的资源。

授权

授权用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

授权的目的是确保用户只能访问他们被授权的资源,保护系统的安全性和完整性。

1.3 会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前

用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

session

基于session的认证方式如下图:

基于Session的认证方式是指在用户登录成功后,服务器会为该用户创建一个唯一的会话标识(Session ID),并将该标识存储在服务器的内存或持久化存储中。服务器将Session ID返回给客户端,通常是通过在响应的Cookie中设置。客户端在后续的请求中会将该Session ID携带在请求的Cookie中。服务器在接收到请求后,通过检查请求中的Session ID,确定该请求是属于哪个用户的会话。通过这种方式,服务器可以验证用户的身份,判断用户是否已经登录,并授权用户访问相应的资源。

token

基于token方式如下图:

基于Token的认证方式是指在用户登录成功后,服务器会生成一个Token(令牌),并将该Token返回给客户端。客户端在后续的请求中会将该Token携带在请求的Header或参数中。服务器在接收到请求后,通过解析请求中的Token,可以获取用户的身份信息,并进行认证和授权。服务器可以使用对称加密或非对称加密算法生成Token,并使用密钥进行签名,以确保Token的安全性和完整性。

基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;

基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。如今移动互联网时代更多类型的客户端

需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.4 授权数据模型

如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型。

授权可简单理解为Who对What(which)进行How操作,包括如下:

Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。 What(沃特),即资源(Resource),

如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于系统功

能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息都属于实体资源(数据资源),实

体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001的商品为资源实例。 How,权限/许可

(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方

法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。

主体、资源、权限关系如下图

主体、资源、权限相关的数据模型如下:

主体(用户id、账号、密码、...)

资源(资源id、资源名称、访问地址、...)

权限(权限id、权限标识、权限名称、资源id、...)

角色(角色id、角色名称、...)

角色和权限关系(角色id、权限id、...)

主体(用户)和角色关系(用户id、角色id、...)

主体(用户)、资源、权限关系如下图:

通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源id、资源名称、访问地址、...)

权限(权限id、权限标识、权限名称、资源id、...)

合并为:

权限(权限id、权限标识、权限名称、资源名称、资源访问地址、...)

修改后数据模型之间的关系如下图:

权限打包给了角色(每个角色都有一组权限),而角色分配给用户(一个用户可以拥有多个角色)

角色就是对权限的一个打包

最少包括五张表(用户表.角色表.用户角色表,权限表,角色权限表)

1.5 RBAC

1.5.1 基于角色控制

RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查

询企业运营报表,查询员工工资信息等,访问控制流程如下:

根据上图中的判断逻辑,授权代码可表示如下:

if(主体.hasRole("总经理角色id")){
	查询工资
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是

总经理或部门经理”,修改代码如下:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
	查询工资
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

1.5.2 基于资源的控制

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须

具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

上图代码

if(主体.hasPermission("查询工资权限标识1")){
查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改授权代码,系统可扩展性强。

加密

加密一般使用的散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密文,不能逆推)

Spring Security提供多种密码加密方案,基本上都实现了PasswordEncoder接口,官方推荐使用BCryptPasswordEncoder

BCryptPasswordEncoder基于Blowfish密码哈希算法,通过多次迭代和随机盐值的加入,提供了较高的安全性。它的主要特点包括:

  1. 随机盐值:每次对密码进行哈希处理时,BCryptPasswordEncoder都会生成一个随机的盐值,将盐值与密码进行组合,增加了密码的复杂度和安全性。

  2. 迭代次数:BCryptPasswordEncoder允许指定哈希处理的迭代次数,迭代次数越多,计算哈希值的成本越高,从而增加了暴力破解的难度。

  3. 固定长度哈希值:BCryptPasswordEncoder生成的哈希值长度固定为60个字符,无论原始密码的长度如何,哈希值的长度始终保持一致。

  4. 自动处理盐值和哈希值的存储:BCryptPasswordEncoder在生成哈希值时,会将盐值和哈希值合并为一个字符串,并自动处理存储到数据库中。

使用BCryptPasswordEncoder进行密码加密和验证时,可以按照以下步骤进行:

  1. 创建BCryptPasswordEncoder对象:BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

  2. 对密码进行加密:String encodedPassword = passwordEncoder.encode(password);

  3. 验证密码:boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);

通过使用BCryptPasswordEncoder,可以确保用户密码的安全性,增加密码的复杂度和抵抗暴力破解的能力。

代码演示

 @Resource
 private BCryptPasswordEncoder bCryptPasswordEncoder;
 ​
     @Test
     void testPassword() {
         String password = "123";
         String encode = bCryptPasswordEncoder.encode(password);
         System.out.println("加密后的"+encode);
         boolean matches = bCryptPasswordEncoder.matches(password, encode);
         System.out.println("是否匹配"+matches);
     }

三、入门案例

期待已久了吧! 这不就来了

3.1 创建一个boot项目

3.2 导入依赖

   <dependencies>
        <!--  aspectj的织入  -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
        <!--         jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--         验证码 -->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
            <exclusions>
                <exclusion>
                    <artifactId>javax.servlet-api</artifactId>
                    <groupId>javax.servlet</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--  mysql驱动  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>
        <!--  连接池  -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.16</version>
        </dependency>
        <!--  mybatis  -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </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>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

3.2 编写application.properties

spring.application.name=securityTrialLecture

server.port=8080


#-----------------------------Mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#mybatis.config-location=classpath:mybatis-config.xml
mybatis.type-aliases-package=com.kk.model
mybatis.mapper-locations=classpath:mapper/*.xml


#-----------------------------Mysql
spring.datasource.url=jdbc:mysql://localhost:3306/securitytest?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456


server.servlet.session.timeout=1m


访问http://localhost:8081/login

3.3 编写控制层

@Controller
public class IndexController {
@GetMapping("index")
@ResponseBody
public String index(){
	return "success";
 }
}

再次访问

logout退出登录

密码错误

四、基本原理

SpringSecurity 本质是一个过滤器链:

从启动是可以获取到过滤器链:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

org.springframework.security.web.context.SecurityContextPersistenceFilter 

org.springframework.security.web.header.HeaderWriterFilter

org.springframework.security.web.csrf.CsrfFilter

org.springframework.security.web.authentication.logout.LogoutFilter 

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 

org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 

org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

org.springframework.security.web.savedrequest.RequestCacheAwareFilter

org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

org.springframework.security.web.authentication.AnonymousAuthenticationFilter 

org.springframework.security.web.session.SessionManagementFilter 

org.springframework.security.web.access.ExceptionTranslationFilter 

org.springframework.security.web.access.intercept.FilterSecurityInterceptor

代码底层流程:重点看三个过滤器:

FilterSecurityInterceptor:

是一个方法级的权限过滤器, 基本位于过滤链的最底部。

filter 已经应用于此请求,并且用户希望我们观察每个请求一次的处理,因此不要重新执行安全检查 filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());

super.beforeInvocation(filterInvocation) 表示查看之前的 filter 是否通过。

filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse());表示真正的调用后台的服务。

ExceptionTranslationFilter

是个异常过滤器,用来处理在认证授权过程中抛出的异常

UsernamePasswordAuthenticationFilter

对/login 的 POST 请求做拦截,校验表单中用户名,密码。

UserDetailsService 接口讲解

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:

返回值 UserDetails

这个类是系统默认的用户“主体

// 表示获取登录用户所有权限

Collection<? **extends** GrantedAuthority> getAuthorities();

// 表示获取密码

String getPassword();

// 表示获取用户名

String getUsername();

// 表示判断账户是否过期

**boolean** isAccountNonExpired();

// 表示判断账户是否被锁定

**boolean** isAccountNonLocked();

// 表示凭证{密码}是否过期

**boolean** isCredentialsNonExpired();

// 表示当前用户是否可用

**boolean** isEnabled();

User类

方法参数 username

表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫 username,否则无法接收。

PasswordEncoder

// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
	return false;
}

接口实现类

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。

BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10.

演示:

@Test
public void test01(){
// 创建密码解析器
BCryptPasswordEncoder bCryptPasswordEncoder = new 
BCryptPasswordEncoder();
// 对密码进行加密
String atguigu = bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+atguigu);
//判断原字符加密后和加密之前是否匹配
boolean result = bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}

五、 自定义登录

1 前端页面

新建login.html页面 ,放在static下面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title></head>
<body>
<form action="/login" method="post">
    <input type="text" name="username"/>
    <input type="password" name="password"/>
    <input type="submit" value="提交"/>
</form>
</body>
</html>

2 修改配置类

配置类需要继承WebSecurityConfigurerAdapte,并重写 configure 方法。直接访问localhost:8081/hello,就会跳到自己定义的登录页面。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
      // 注入 PasswordEncoder 类到 spring 容器中
      @Bean
      public PasswordEncoder passwordEncoder(){
           return new BCryptPasswordEncoder();
      }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证
        http.formLogin()
                //代表的是定义的from表单提交的url
                .loginProcessingUrl("/login")
                //登录后跳转
                .successForwardUrl("/success")
                //登录失败跳转地址
                .failureForwardUrl("/fail")
                //定义跳转页面的url
                .loginPage("/login.html");
        //http.authorizeRequests() 主要是对url进行控制,也就是我们所说的授权(访问控制)
        //authorizeRequests  开始开始配置请求校验
        http.authorizeRequests()
                /**
                 * anyRequest 控制访问url匹配
                 * mvcMatchers()适用于配置了 servletPath 的情况   匹配的内容
                 * permitAll():允许
                 */

                //放行的路径
                .antMatchers("/login.html","/fail.html").permitAll()
                // 任何请求都需要授权,注意顺序 从上至下
                .anyRequest().authenticated();
        // 解除跨域保护
        http.csrf().disable();
    }
}

3.修改业务逻辑层

SpringUserDetailsService

package com.kk.service;


import com.kk.config.SecurityConfig;
import com.kk.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class SpringUserDetailsService implements UserDetailsService {

    @Resource
    private SecurityConfig securityConfig;

    @Resource
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws
        UsernameNotFoundException {
        //1. 查询数据库判断用户名是否存在,如果不存在抛出 UsernameNotFoundException
        if(!username.equals("admin")){
            throw new UsernameNotFoundException("用户名不存在");
        }
        //把查询出来的密码进行解析,或直接把 password 放到构造方法中。
        // 理解:password 就是数据库中查询出来的密码,查询出来的内容不是 123
        String password = securityConfig.passwordEncoder().encode("123");
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

4.控制层

@RestController
public class IndexController {
   @GetMapping("index")
   @ResponseBody
   public String index(){
       return "hello security";
   }

    @PostMapping("/fail")
    public String fail(){
        return "redirect:/fail.html";
    }
    @RequestMapping("/success")
    public String success(){
        return "redirect:/success.html";
    }
}

六、 前后端分离处理

1 请求成功

  • .successHandler()

.successHandler(((request, response, authentication) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证成功");
                    map.put("status", 200);
                    map.put("用户信息", authentication.getPrincipal());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))

2 请求失败

  • .failureHandler()

.failureHandler(((request, response, exception) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证失败");
                    map.put("status", 500);
                    map.put("异常信息", exception.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))

3.重新编写securityConfig

package com.kk.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.HashMap;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 注入 PasswordEncoder 类到 spring 容器中
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证
        http.formLogin()
                //代表的是定义的from表单提交的url
                .loginProcessingUrl("/login")
                //登录后跳转
          //      .successForwardUrl("/success")
                //登录失败跳转地址
         //       .failureForwardUrl("/fail")
                //定义跳转页面的url
                .loginPage("/login.html")
                .successHandler(((request, response, authentication) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证成功");
                    map.put("status", 200);
                    map.put("用户信息", authentication.getPrincipal());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))
                .failureHandler(((request, response, exception) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证失败");
                    map.put("status", 500);
                    map.put("异常信息", exception.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))
                    ;
        //http.authorizeRequests() 主要是对url进行控制,也就是我们所说的授权(访问控制)
        //authorizeRequests  开始开始配置请求校验
        http.authorizeRequests()
                /**
                 * anyRequest 控制访问url匹配
                 * mvcMatchers()适用于配置了 servletPath 的情况   匹配的内容
                 * permitAll():允许
                 */

                //放行的路径
                .antMatchers("/login.html","/fail.html").permitAll()
                // 任何请求都需要授权,注意顺序 从上至下
                .anyRequest().authenticated();
        // 解除跨域保护
        http.csrf().disable();
    }
}

七.访问控制 url

url匹配其实就是权限匹配

1 anyRequest()

在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所 有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。

2 antMatcher()

public C antMatchers(String... antPatterns)

参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。规则如下:

? 匹配一个字符

*匹配 0 个或多个字符

** 匹配 0 个或多个目录

放行js文件夹下的所有资源: .antMatchers("/js/**").permitAll()

放行所有js资源 antMatchers("/**/*.js").permitAll()

3 regexMatchers()

使用正则表达式进行匹配。和 antMatchers()主要的区别就是参 数,antMatchers()参数是 ant 表达式,regexMatchers()参数是正则表 达式

.regexMatchers(".+[.]js").permitAll()

  1. permitAll()

    permitAll()表示所匹配的 URL 任何人都允许访问。

八、 内置访问控制方法

1 permitAll()

permitAll()表示所匹配的 URL 任何人都允许访问。

2 authenticated()

authenticated()表示所匹配的 URL 都需要被认证才能访问。

3 anonymous()

anonymous()表示可以匿名访问匹配的 URL。和 permitAll()效果类 似,只是设置为 anonymous()的 url 会执行 filter 链中

4 denyAll()

denyAll()表示所匹配的 URL 都不允许被访问。

5 rememberMe()

被“remember me”的用户允许访问

九、基于角色授权

除了之前讲解的内置权限控制。Spring Security 中还支持很多其 他权限控制。这些方法一般都用于用户已经被认证后,判断用户是否 具有特定的要求

1 hasAuthority(String)

判断权限的

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑 中创建 User 对象时指定的。

下图中 admin 就是用户的权限。admin 严格区分大小写。

在配置类配置hasAuthorityfangenticated()

.antMatchers("/detail").hasAuthority("admin")
.antMatchers("/index").hasAuthority("kk")

访问http://localhost:80801/detail会跳转到认证页面认证,然后再访问http://localhost:8081/detail会出现403,禁止访问,但是访问http://localhost:8081/index 是可以的

2 hasAnyAuthority(String ...)

可以放多个参数

如果用户具备给定权限中某一个,就允许访问。

.antMatchers("/index").hasAnyAuthority("kk","admin")

3 hasRole(String)

如果用户具备给定角色就允许访问。否则出现 403。 参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创 建 User 对象时给 User 赋予的授权。 在给用户赋予角色时角色需要以:ROLE_ 开头,后面添加角色名 称。例如:ROLE_abc 其中 abc 是角色名,ROLE_ 是固定的字符开头。 使用 hasRole()时参数也只写 abc 即可。否则启动报错。 给用户赋予角色,角色和权限区分就是通过ROLE_ 来区分的。

4 hasAnyRole(String ...)

如果用户具备给定角色的任意一个,就允许被访问

十、 403处理

 // 异常处理
http.exceptionHandling()
    .accessDeniedHandler(((request, response, accessDeniedException) -> {
        HashMap<String, Object> map = new HashMap<>();
        map.put("msg", "禁止访问");
        map.put("status", 403);
        map.put("异常信息", accessDeniedException.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(map);
        response.getWriter().println(s);
    }));

十一、 实战

1.建数据库表

  1. SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for menu
    -- ----------------------------
    DROP TABLE IF EXISTS `menu`;
    CREATE TABLE `menu`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `pattern` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of menu
    -- ----------------------------
    INSERT INTO `menu` VALUES (1, '/admin/**');
    INSERT INTO `menu` VALUES (2, '/user/**');
    INSERT INTO `menu` VALUES (3, '/guest/**');
    
    -- ----------------------------
    -- Table structure for menu_role
    -- ----------------------------
    DROP TABLE IF EXISTS `menu_role`;
    CREATE TABLE `menu_role`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `rid` int(11) NULL DEFAULT NULL,
      `mid` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of menu_role
    -- ----------------------------
    INSERT INTO `menu_role` VALUES (1, 1, 1);
    INSERT INTO `menu_role` VALUES (2, 1, 2);
    INSERT INTO `menu_role` VALUES (3, 2, 2);
    INSERT INTO `menu_role` VALUES (4, 3, 3);
    
    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES (1, 'product', '商品管理员');
    INSERT INTO `role` VALUES (2, 'admin', '系统管理员');
    INSERT INTO `role` VALUES (3, 'user', '用户管理员');
    
    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
      `enabled` tinyint(1) NULL DEFAULT NULL,
      `accountNonExpired` tinyint(1) NULL DEFAULT NULL,
      `accountNonLocked` tinyint(1) NULL DEFAULT NULL,
      `credentialsNonExpired` tinyint(1) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES (1, 'root', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
    INSERT INTO `user` VALUES (2, 'admin', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
    INSERT INTO `user` VALUES (3, 'han', '$2a$10$YeilFczw8sQ1RBthIFLRW.pI/KkdbDofOmZ0w5Wfq9mMMyL3ylC/.', 1, 1, 1, 1);
    
    -- ----------------------------
    -- Table structure for user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role`  (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `uid` int(11) NULL DEFAULT NULL,
      `rid` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
    
    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES (1, 1, 1);
    INSERT INTO `user_role` VALUES (2, 1, 2);
    INSERT INTO `user_role` VALUES (3, 2, 2);
    INSERT INTO `user_role` VALUES (4, 3, 3);
    
    SET FOREIGN_KEY_CHECKS = 1;

2.依赖

上述有

3.配置

spring.application.name=securityTrialLecture

server.port=8081


#-----------------------------Mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
#mybatis.config-location=classpath:mybatis-config.xml
mybatis.type-aliases-package=com.kk.model
mybatis.mapper-locations=classpath:mapper/*.xml


#-----------------------------Mysql
spring.datasource.url=jdbc:mysql://localhost:3306/securitytest?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456


server.servlet.session.timeout=1m

#spring.security.user.name=kk
#spring.security.user.password=123

4.实体类

user

package com.kk.model;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;

/**
 * 用户信息
 */
public class User implements UserDetails {

    private Integer id;
    private String username;  //用户名
    private String password;  //密码
    private boolean accountNonExpired;   //是否没过期
    private boolean accountNonLocked;   //是否没被锁定
    private boolean credentialsNonExpired;  //密码是否没过期
    private boolean enabled;  //账号是否可用
    private Collection<? extends GrantedAuthority> authorities;  //用户的权限集合




    public Integer getId() {
        return id;
    }

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

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

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

    public void setAccountNonExpired(boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

    public void setAccountNonLocked(boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

    public void setCredentialsNonExpired(boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;   //暂时未用到,直接返回true,表示账户未过期
    }

    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;   //暂时未用到,直接返回true,表示账户未被锁定
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;   //暂时未用到,直接返回true,表示账户密码未过期
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

  • Role

/**
 * 角色信息
 */
public class Role {
    /**
     * id
     */
    private Integer id;
    /**
     * 角色名
     */
    private String name;
    /**
     * 角色说明
     */
    private String content;

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name == null ? null : name.trim();
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content == null ? null : content.trim();
    }
}    
  • Menu类

public class Menu {

    private Integer id;

    private String pattern;

    private List<Role> roles;

    public Integer getId() {
        return id;
    }

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

    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

5 .mapper

package com.kk.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kk.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

@Mapper
public interface UserMapper{
    User selectByUsername(String usename);

    List<String> selectRolesByUserId(Integer id);

    Integer updatePassword(@Param("username") String username, @Param("password") String password);

    List<String> selectMenuByRoles(@Param("roleCodes")List<String> roleCodes);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.kk.mapper.UserMapper" >

  <sql id="Base_Column_List" >
    id, username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired
  </sql>
    <update id="updatePassword">
        update user set password=#{password} where username=#{username}
    </update>

    <select id="selectByUsername" resultType="com.kk.model.User">
    select
    <include refid="Base_Column_List" />
    from user
    where username = #{username}
  </select>
  <select id="selectRolesByUserId" resultType="string">
    select
        r.name
    from
        role r
     left join
        user_role ur
     on r.id = ur.rid
     where ur.uid=#{uid}
  </select>
    <select id="selectMenuByRoles" resultType="java.lang.String">
        select pattern from menu m
        left join menu_role rm on m.id=rm.mid
        left join role r on r.id=rm.rid
        where
        <foreach collection="roleCodes" open="r.name in(" item="name" close=")" separator=",">
            #{name}
        </foreach>
    </select>

</mapper>

6 UserDetailsService实现类

package com.kk.service;


import com.kk.config.SecurityConfig;
import com.kk.mapper.UserMapper;
import com.kk.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SpringUserDetailsService implements UserDetailsService {

    @Resource
    private SecurityConfig securityConfig;

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.selectByUsername(username);

        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }

        //获得用户角色列表id
        List<String> roleCodes = userMapper.selectRolesByUserId(user.getId());
        //通过角色列表获取权限列表
        List<String> menus = userMapper.selectMenuByRoles(roleCodes);

        roleCodes = roleCodes.stream().distinct().map(r->"ROLE_"+r).collect(Collectors.toList());

        menus.addAll(roleCodes);
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",",menus)));
        return user;
    }
}

7.配置类

package com.kk.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.HashMap;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
      // 注入 PasswordEncoder 类到 spring 容器中
      @Bean
      public PasswordEncoder passwordEncoder(){
           return new BCryptPasswordEncoder();
      }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证
        http.formLogin()
                //代表的是定义的from表单提交的url
                .loginProcessingUrl("/login")
//                //登录后跳转
//                .successForwardUrl("/success")
//                //登录失败跳转地址
//                .failureForwardUrl("/fail")
                //认证成功处理
                .successHandler(((request, response, authentication) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证成功");
                    map.put("status", 200);
                    map.put("用户信息", authentication.getPrincipal());
                    response.setContentType("application/json;charset=UTF-8");
                    //后端Servlet想ajax传递json格式数据
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))
                // 认证失败处理
                .failureHandler(((request, response, exception) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "认证失败");
                    map.put("status", 500);
                    map.put("异常信息", exception.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }))

                //定义跳转页面的url
                .loginPage("/login.html");
        // 异常处理
        http.exceptionHandling()
                .accessDeniedHandler(((request, response, accessDeniedException) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "禁止访问");
                    map.put("status", 403);
                    map.put("异常信息", accessDeniedException.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }));
        //http.authorizeRequests() 主要是对url进行控制,也就是我们所说的授权(访问控制)
        //authorizeRequests  开始开始配置请求校验
        //拦截器
        http.authorizeRequests()

                /**
                 * anyRequest 控制访问url匹配
                 * mvcMatchers()适用于配置了 servletPath 的情况   匹配的内容
                 * permitAll():允许
                 */

                //放行的路径
                .antMatchers("/login.html","/fail.html").permitAll()
                .antMatchers("/index").hasAuthority("")
                .antMatchers("/detail").hasRole("admin")
                // 任何请求都需要授权,注意顺序 从上至下
                .anyRequest().authenticated();

        // 记住我功能
        http.rememberMe().tokenValiditySeconds(7 * 24 * 3600);
        // 解除跨域保护
        http.csrf().disable();
    }

}

十二、授权

权限其实就是判断某个用户时候具有对指定资源的访问权限,之前我们都是用静态的方式配置的,这里我们用动态的方式,原理就是判断用户访问的路径和我们的权限路径去匹配,看是否相符。

因为授权是在登录后的操作,UserDetailsService在使用loadUserByUsername()方法的时候,已经返回了UserDetails对象,这个对象是包含了所有权限信息的。

修改配置类

package com.xinzhi.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xinzhi.filter.JwtAuthenticationTokenFilter;
import com.xinzhi.service.SpringUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {


    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.logout().logoutSuccessHandler(((request, response, authentication) -> {
            //退出
            HashMap<String, Object> map = new HashMap<>();
            map.put("msg", "退出成功");
            map.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(map);
            response.getWriter().println(s);
        }));

        // 拦截
        http.authorizeHttpRequests()
                //不拦截
                .antMatchers("/login.html", "/fail.html", "/invalidSession.html", "/authentication","/refreshtoken","/logout").permitAll()
//                .antMatchers("/**/*").permitAll()

//        任何请求都必须授权   检测用户有没有权限
                .anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
            // 当前用户的权限信息 比如角色
            Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
            // 当前请求上下文
            // 我们可以获取原始request对象
            HttpServletRequest request = requestAuthorizationContext.getRequest();
            //路径权限
            SimpleGrantedAuthority simpleGrantedAuthority
                    = new SimpleGrantedAuthority(request.getRequestURI());
            //判断能否访问
            boolean contains = authorities.contains(simpleGrantedAuthority);
            return new AuthorizationDecision(contains);
        })
        ;

        // 异常处理
        http.exceptionHandling()
                .accessDeniedHandler(((request, response, accessDeniedException) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "禁止访问");
                    map.put("status", 403);
                    map.put("异常信息", accessDeniedException.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }));
        // session管理
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 记住我功能
        http.rememberMe().tokenValiditySeconds(7 * 24 * 3600);
        //跨域 前后端分离加
        http.cors(c -> {
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                //不管是哪个ip访问
                config.setAllowedOrigins(Arrays.asList("*"));
                //不管是什么请求方式都访问
                config.setAllowedMethods(Arrays.asList("*"));
                config.addAllowedHeader("*");
                return config;
            };
            c.configurationSource(source);
        });
        // 解除跨域保护
        http.csrf().disable();
        //跨站攻击CSRF    authentication身份认证
        http.csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .ignoringAntMatchers("/authentication");
    }

    /**
     * 注入加密类
     *
     * @return
     */
    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    private SpringUserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        // 加密方式认证
        builder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

}

十三、退出登录

    http.logout().logoutSuccessHandler(((request, response, authentication) -> {
            HashMap<String, Object> map = new HashMap<>();
            map.put("msg", "退出成功");
            map.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(map);
            response.getWriter().println(s);
        }));

十四 验证码

导入依赖

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
    <exclusions>
        <exclusion>
            <artifactId>javax.servlet-api</artifactId>
            <groupId>javax.servlet</groupId>
        </exclusion>
    </exclusions>
</dependency>

1 实体类 CaptchaImageVO

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class CaptchaImageVO {

    //验证码文字
    private String code;
    //验证码失效时间
    private LocalDateTime expireTime;

    public CaptchaImageVO(String code, int expireAfterSeconds){
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
    }

    //验证码是否失效
    public boolean isExpried() {
        return LocalDateTime.now().isAfter(expireTime);
    }

    public String getCode() {
        return code;
    }
}

2 kaptcha.properties配置文件

kaptcha.border=no
kaptcha.border.color=105,179,90
kaptcha.image.width=100
kaptcha.image.height=45
kaptcha.session.key=code
kaptcha.textproducer.font.color=blue
kaptcha.textproducer.font.size=35
kaptcha.textproducer.char.length=4   //验证码几个字就是几
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑

3 配置类CaptchaConfig

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

import java.util.Properties;

@Component
@PropertySource(value = {"classpath:kaptcha.properties"})
public class CaptchaConfig {

    @Value("${kaptcha.border}")
    private String border;
    @Value("${kaptcha.border.color}")
    private String borderColor;
    @Value("${kaptcha.textproducer.font.color}")
    private String fontColor;
    @Value("${kaptcha.image.width}")
    private String imageWidth;
    @Value("${kaptcha.image.height}")
    private String imageHeight;
    @Value("${kaptcha.textproducer.char.length}")
    private String charLength;
    @Value("${kaptcha.textproducer.font.names}")
    private String fontNames;
    @Value("${kaptcha.textproducer.font.size}")
    private String fontSize;
    @Value("${kaptcha.session.key}")
    private String sessionKey;

    @Bean(name = "captchaProducer")
    public DefaultKaptcha getKaptchaBean() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border", border);
        properties.setProperty("kaptcha.border.color", borderColor);
        properties.setProperty("kaptcha.textproducer.font.color", fontColor);
        properties.setProperty("kaptcha.image.width", imageWidth);
        properties.setProperty("kaptcha.image.height", imageHeight);
        properties.setProperty("kaptcha.session.key", sessionKey);
        properties.setProperty("kaptcha.textproducer.char.length", charLength);
        properties.setProperty("kaptcha.textproducer.font.names", fontNames);
        properties.setProperty("kaptcha.textproducer.font.size",fontSize);
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }

}

4 CaptchaController

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.xinzhi.model.CaptchaImageVO;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;

@RestController
public class CaptchaController {

    @Resource
    DefaultKaptcha captchaProducer;
    /**
     * 获取验证码
     */
    @RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
    public void kaptcha(HttpSession session, HttpServletResponse response) throws Exception {

        response.setDateHeader("Expires", 0);
        //no-cache不缓存的   max-age最大的
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");

        String capText = captchaProducer.createText();
        System.out.println(capText);
        CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
        //将验证码存到session
        session.setAttribute("code", capText);
        //将图片返回给前端
        try(ServletOutputStream out = response.getOutputStream();) {
            BufferedImage bi = captchaProducer.createImage(capText);
            ImageIO.write(bi, "jpg", out);
            out.flush();
        }//使用try-with-resources不用手动关闭流
    }
}

5 CaptchaCodeFilter

import com.alibaba.druid.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;

@Component                           //之刷新一次得的过滤器
public class CaptchaCodeFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        // 必须是登录的post请求才能进行验证,其他的直接放行
        if(StringUtils.equals("/login",request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){

            //1.验证谜底与用户输入是否匹配
            if (!validate(new ServletWebRequest(request), response)){
                return;
            }
        }
        //通过校验,就放行
        filterChain.doFilter(request,response);

    }

    private Boolean validate(ServletWebRequest request,HttpServletResponse response) throws ServletRequestBindingException, IOException {

        HttpSession session = request.getRequest().getSession();
        //获取用户登录界面输入的captchaCode
        String codeInRequest = ServletRequestUtils.getStringParameter(
                request.getRequest(),"captchaCode");
        if(StringUtils.isEmpty(codeInRequest)){
            HashMap<String, Object> map = new HashMap<>();
            map.put("msg", "验证码不能为空");
            map.put("status", 500);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(map);
            response.getWriter().println(s);
            return false;
        }

        // 获取session池中的验证码谜底
        String codeInSession = (String)
                session.getAttribute("code");
        // 请求验证码校验
        if(!StringUtils.equals(codeInSession, codeInRequest)) {
            HashMap<String, Object> map = new HashMap<>();
            map.put("msg", "验证码不匹配");
            map.put("status", 500);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(map);
            response.getWriter().println(s);
            return false;
        }
        return true;
    }
}

6 过滤器替换

配置文件中添加

http.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class);

7 前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title></head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username"/> <br>
    密码: <input type="password" name="password"/> <br>
    验证码:<input type="text" name="captchaCode"/><img src="http://localhost:8080/kaptcha" id="kaptcha" width="110px" height="40px"/> <br>
    <input type="checkbox" name="remember-me" value="on"/>记住密码 <br>
    <input type="submit" value="提交"/>
</form>

<a href="/login/qq">QQ登录</a>

<script>
    window.οnlοad=function(){
        var kaptchaImg = document.getElementById("kaptcha");
        kaptchaImg.onclick = function(){
            kaptchaImg.src = "http://localhost:8080/kaptcha?" + Math.floor(Math.random() * 100)
        }
    }
</script>
</body>
</html>

8 验证码controller

package com.xinzhi.controller;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.xinzhi.model.CaptchaImageVO;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.awt.image.BufferedImage;

@RestController
public class CaptchaController {

    @Resource
    DefaultKaptcha captchaProducer;
    /**
     * 获取验证码
     */
    @RequestMapping(value = "/kaptcha", method = RequestMethod.GET)
    public void kaptcha(HttpSession session, HttpServletResponse response) throws Exception {

        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");

        String capText = captchaProducer.createText();
        System.out.println(capText);
        CaptchaImageVO captchaImageVO = new CaptchaImageVO(capText,2 * 60);
        //将验证码存到session
        session.setAttribute("code", capText);
        //将图片返回给前端
        try(ServletOutputStream out = response.getOutputStream();) {
            BufferedImage bi = captchaProducer.createImage(capText);
            ImageIO.write(bi, "jpg", out);
            out.flush();
        }//使用try-with-resources不用手动关闭流
    }

}

9 配置类中验证码路径放行

.antMatchers("/login.html", "/fail.html", "/invalidSession.html","/kaptcha").permitAll()

十五整合JWT

JWT原理

整合后的原理

认证流程

代码

引入依赖
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>
创建jwt工具类
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Data
@Component
public class JwtTokenUtil {
    private String secret="xinzhi";
    private Long expiration=3600000L;
    private String header="JWTHeaderName";


    /**
     * 生成token令牌
     *
     * @param userDetails 用户
     * @return 令token牌
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        //获取用户名
        claims.put("sub", userDetails.getUsername());
        // 创建时间
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @param token 令牌
     * @return 用户名
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }


    /**
     * 从claims生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

}

异常
public class CustomException extends RuntimeException {
    //异常错误编码
    private int code ;
    //异常信息
    private String message;

    private CustomException(){}

    public CustomException(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    @Override
    public String getMessage() {
        return message;
    }
}

JwtAuthService
import com.xinzhi.exception.CustomException;
import com.xinzhi.utils.JwtTokenUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class JwtAuthService {

    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private JwtTokenUtil jwtTokenUtil;

    public UserDetails login(String username, String password) throws CustomException {
        try{
            //使用用户名密码进行登录验证
            UsernamePasswordAuthenticationToken upToken =
                    new UsernamePasswordAuthenticationToken( username, password );
            Authentication authentication = authenticationManager.authenticate(upToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }catch(AuthenticationException e){
            throw new CustomException(500, "用户名或密码不正确");
        }

        
        UserDetails userDetails = userDetailsService.loadUserByUsername( username );
        return userDetails;
//        return jwtTokenUtil.generateToken(userDetails);
    }

    public String refreshToken(String oldToken) {
        if (!jwtTokenUtil.isTokenExpired(oldToken)) {
            return jwtTokenUtil.refreshToken(oldToken);
        }
        return null;
    }
}

JwtAuthController
import com.xinzhi.exception.CustomException;
import com.xinzhi.model.Result;
import com.xinzhi.service.Impl.JwtAuthService;
import com.xinzhi.utils.JwtTokenUtil;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.annotation.Resource;
import java.util.HashMap;

@RestController
public class JwtAuthController {

    @Resource
    private JwtAuthService jwtAuthService;

    @Resource
    private JwtTokenUtil jwtTokenUtil;

    @PostMapping(value = "/authentication")
    public Result login(ServletWebRequest request) throws ServletRequestBindingException {
        String username = ServletRequestUtils.getStringParameter(
                request.getRequest(),"username");
        String password = ServletRequestUtils.getStringParameter(
                request.getRequest(),"password");
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            return Result.error("用户名密码不能为空");
        }
        try{
            UserDetails userDetails = jwtAuthService.login(username, password);
            String token = jwtTokenUtil.generateToken(userDetails);
            HashMap<String, Object> map1 = new HashMap<>();
            map1.put("token", token);
            map1.put("user", userDetails.getUsername());
            return Result.ok(map1);
        }catch(CustomException e){
            return Result.error("认证失败");
        }
    }

    @PostMapping(value = "/refreshtoken")
    public Result refresh(@RequestHeader("JWTHeaderName") String token) {
        return Result.ok(jwtAuthService.refreshToken(token));
    }

}
JwtAuthenticationTokenFilter
import com.xinzhi.service.Impl.SpringUserDetailsService;
import com.xinzhi.utils.JwtTokenUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    SpringUserDetailsService myUserDetailsService;

    @Resource
    JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        String jwtToken = request.getHeader(jwtTokenUtil.getHeader());
        if(!StringUtils.isEmpty(jwtToken)){
            String username = jwtTokenUtil.getUsernameFromToken(jwtToken);

            //如果可以正确的从JWT中提取用户信息,并且该用户未被授权
            if(username != null &&
                    SecurityContextHolder.getContext().getAuthentication() == null){

                UserDetails userDetails = myUserDetailsService.loadUserByUsername(username);

                if(jwtTokenUtil.validateToken(jwtToken,userDetails)){
                    //给使用该JWT令牌的用户进行授权
                    UsernamePasswordAuthenticationToken authenticationToken
                            = new UsernamePasswordAuthenticationToken(userDetails,null,
                            userDetails.getAuthorities());

                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request,response);

    }
}
  • 配置类

删除表单认证方式,

允许jwt认证路径 .antMatchers("/authentication","/refreshtoken").permitAll()

清除session 并且用jwt的过滤器替换UsernamePasswordAuthenticationFilter

package com.xinzhi.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.xinzhi.filter.CaptchaCodeFilter;
import com.xinzhi.filter.JwtAuthenticationTokenFilter;
import com.xinzhi.service.SpringUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CaptchaCodeFilter captchaCodeFilter;

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证
//        http.formLogin()
//                .loginProcessingUrl("/authentication")
//                .successForwardUrl("/index")
//                .failureForwardUrl("/fail") //登录失败跳转地址
                //认证成功处理
//                .successHandler(((request, response, authentication) -> {
//                    HashMap<String, Object> map = new HashMap<>();
//                    map.put("msg", "认证成功");
//                    map.put("status", 200);
//                    map.put("用户信息", authentication.getPrincipal());
//                    response.setContentType("application/json;charset=UTF-8");
//                    //后端Servlet想ajax传递json格式数据
//                    String s = new ObjectMapper().writeValueAsString(map);
//                    response.getWriter().println(s);
//                }))
//                // 认证失败处理
//                .failureHandler(((request, response, exception) -> {
//                    HashMap<String, Object> map = new HashMap<>();
//                    map.put("msg", "认证失败");
//                    map.put("status", 500);
//                    map.put("异常信息", exception.getMessage());
//                    response.setContentType("application/json;charset=UTF-8");
//                    String s = new ObjectMapper().writeValueAsString(map);
//                    response.getWriter().println(s);
//                }))
//                .loginPage("/login.html");
        http.logout().logoutSuccessHandler(((request, response, authentication) -> {
            HashMap<String, Object> map = new HashMap<>();
            map.put("msg", "退出成功");
            map.put("status", 200);
            response.setContentType("application/json;charset=UTF-8");
            String s = new ObjectMapper().writeValueAsString(map);
            response.getWriter().println(s);
        }));

        // 拦截
        http.authorizeHttpRequests()
                //不拦截  ,"/kaptcha"这个是图片片的路径
                .antMatchers("/login.html", "/fail.html","/authentication", "/invalidSession.html",
                        "/kaptcha","/refreshtoken").permitAll()
                //                .antMatchers("/detail.html").hasRole("admin")
                //                .antMatchers("/index.html").hasRole("super")
                //任何请求都必须授权   检测用户有没有权限
                .anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
            // 当前用户的权限信息 比如角色
            Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
            // 当前请求上下文
            // 我们可以获取原始request对象
            HttpServletRequest request = requestAuthorizationContext.getRequest();
            //路径权限
            SimpleGrantedAuthority simpleGrantedAuthority
                    = new SimpleGrantedAuthority(request.getRequestURI());
            //判断能否访问
            boolean contains = authorities.contains(simpleGrantedAuthority);
            return new AuthorizationDecision(contains);
        });

        // 异常处理
        http.exceptionHandling()
                .accessDeniedHandler(((request, response, accessDeniedException) -> {
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("msg", "禁止访问");
                    map.put("status", 403);
                    map.put("异常信息", accessDeniedException.getMessage());
                    response.setContentType("application/json;charset=UTF-8");
                    String s = new ObjectMapper().writeValueAsString(map);
                    response.getWriter().println(s);
                }));
        // session管理
        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

                //.invalidSessionUrl("/invalidSession.html")    //非法超时session跳转页面
//                .maximumSessions(1) // 允许登录的最多用户数量
//                .maxSessionsPreventsLogin(false) // true表示已经登录就不予许再次登录, false表示允许再次登录但是之前的登录账户会被踢下线
//                .expiredSessionStrategy(new CustomExpiredSessionStrategy());
        // 记住我功能
        http.rememberMe().tokenValiditySeconds(7 * 24 * 3600);
        //验证码的过滤器
//        http.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class);
        //跨域 前后端分离加
        http.cors(c -> {
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                //不管是哪个ip访问
                config.setAllowedOrigins(Arrays.asList("*"));
                //不管是什么请求方式都访问
                config.setAllowedMethods(Arrays.asList("*"));
                config.addAllowedHeader("*");
                return config;
            };
            c.configurationSource(source);
        });
        // 解除跨域保护
//        http.csrf().disable();
        //跨站攻击CSRF    authentication身份认证
        http.csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .ignoringAntMatchers("/authentication");
    }

    /**
     * 注入加密类
     *
     * @return
     */
    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    private SpringUserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder builder) throws Exception {
        // 加密方式认证
        builder.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }

}

修改登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title></head>
<body>
<form action="/authentication" method="post">
    用户名:<input type="text" name="username"/> <br>
    密码: <input type="password" name="password"/> <br>
    验证码:<input type="text" name="captchaCode"/><img src="/kaptcha" id="kaptcha" width="110px" height="40px"/> <br>
    <input type="checkbox" name="remember-me" value="on"/>记住密码 <br>
    <input type="submit" value="提交"/>
</form>

<script>
    window.οnlοad=function(){
        var kaptchaImg = document.getElementById("kaptcha");
        kaptchaImg.onclick = function(){
            kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
        }
    }
</script>
</body>
</html>

跨域

为什么要跨域

当页面执行脚本时,浏览器会检查访问的资源是否同源,如果不是,就会报错。跨域是客户端错

误。同源策略主要防范的是CSRF攻击由于同源策略的存在,当A网站试图获取B网站的cookie的

时候,浏览器会提示一个错误。同理除了不能获取cookie,同源策略也限制的DOM节点的访问,

因为与其获取cookie,直接监控input输入框,获取用户的账号密码岂不是更简单。

配置类

http.cors(c -> {
            CorsConfigurationSource source = request -> {
                CorsConfiguration config = new CorsConfiguration();
                config.setAllowedOrigins(Arrays.asList("*"));
                config.setAllowedMethods(Arrays.asList("*"));
                config.addAllowedHeader("*");
                return config;
            };
            c.configurationSource(source);
        });

CSRF 跨站攻击

CSRF(Cross-Site Request Forgery)跨站请求伪造是一种常见的Web安全漏洞,攻击者利用受害者在已登录的情况下发起恶意请求,以执行未经授权的操作。

你可以这样来理解: 攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等

CSRF攻击的原理是攻击者利用受害者的身份认证信息(如Cookie)来伪造请求,然后通过诱使受害者点击恶意链接或访问恶意网站来触发攻击。

http.csrf()
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    .ignoringAntMatchers("/authentication");

.csrf 方法表示启用CSRF保护功能,.csrfTokenRepository()方法用于配置CSRF令牌的存储库

  • 使用CookieCsrfTokenRepository生成CSRF Token放入cookie,并设置cookie的HttpOnly=false,允许读取该cookie。这样非浏览器等无法自动维护cookie的客户端可以读取cookie中的CSRF Token,以供后续资源请求中使用。

  • 使用ignoringAntMatchers开放一些不需要进行CSRF防护的访问路径

后台日志记录

 <!-- aspectj的织入 -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.13</version>
        </dependency>
import com.xinzhi.model.User;
import lombok.Data;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

@Component("myAspect")
@Aspect
@Data
public class MyAspect {

    private final static Logger logger= LoggerFactory.getLogger(MyAspect.class);

    //@Before指在切点方法之前执行,也就是在Controller层方法执行之前执行,这里可以通过JoinPoint获取一些有关方法的信息,在这里也可以修改参数的值

    //@Before()括号里设置的是切点方法的名称
    @Before("execution(public * com.xinzhi.controller.*.*(..))")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        User user =(User)(SecurityContextHolder.getContext().getAuthentication().getPrincipal());
        // 记录下请求内容
        logger.info("URL : " + request.getRequestURI().toString());
        logger.info("HTTP_METHOD : " + request.getMethod());
        logger.info("IP : " + request.getRemoteAddr());
        logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
        logger.info("用户 : " + user.getUsername());
    }

}

十六 工作原理

结构总览

Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:

image-20230618144553734

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认**证,也不直接处理用户的授权**,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。

image-20230618144626927

spring Security功能的实现主要是由一系列过滤器链相互配合完成。

image-20230618144641294

下面介绍过滤器链中主要的几个过滤器及其作用:

SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截

器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给

SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好

的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;

UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;

FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;

ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

认证流程

image-20230618144804803

  1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,

封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

  1. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证

  2. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,

身份信息,细节信息,但密码通常会被移除) Authentication 实例。

  1. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过

SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它

的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个

List<AuthenticationProvider> 列表,存放多种认证方式,最终实际的认证工作是由

AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为

DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终

AuthenticationProvider将UserDetails填充至Authentication。

认证核心组件大体关系:

image-20230618144855512

AuthenticationProvider

通过前面的Spring Security**认证流程**我们得知,认证管理器(AuthenticationManager)委托

AuthenticationProvider完成认证工作。

AuthenticationProvider是一个接口,定义如下:

authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。

Spring Security中维护着一个 List<AuthenticationProvider> 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等这样的例子很多。

每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?

我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:

public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

也就是说当**web表单提交用户名密码时,Spring SecurityDaoAuthenticationProvider处理。

最后,我们来看一下Authentication(认证信息)的结构,它是一个接口,我们之前提到的

UsernamePasswordAuthenticationToken就是它的实现之一:

public interface Authentication extends Principal, Serializable { (1)
Collection<? extends GrantedAuthority> getAuthorities(); (2)
Object getCredentials(); (3)
Object getDetails(); (4)
Object getPrincipal(); (5)
boolean isAuthenticated();
void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

(1)Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security

包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。

(2)getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系

列字符串。

(3)getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。

(4)getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地

址和sessionId的值。

(5)getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细

信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。

UserDetailsService

1)认识UserDetailsService

现在咱们现在知道DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个

Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。

DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息

UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定义自定义身份验证

public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

很多人把DaoAuthenticationProvider和UserDetailsService的职责搞混淆,其实UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。而DaoAuthenticationProvider的职责更大,它完成完整的认证流程,同时会把UserDetails填充至Authentication。

上面一直提到UserDetails是用户信息,咱们看一下它的真面目:

public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}

它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。

通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。

Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。

PasswordEncoder

1)认识PasswordEncoder

DaoAuthenticationProvider认证处理器通过UserDetailsService获取到UserDetails后,它是如何与请求

Authentication中的密码做对比呢?

在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过

PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:

而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如

下声明即可,如下:

@Bean

public PasswordEncoder passwordEncoder() {

return NoOpPasswordEncoder.getInstance();

}

NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:

1、用户输入密码(明文 )

2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)

3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通

过,否则校验失败。

NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致

则校验通过,否则 校验失败。

实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感兴趣

的大家可以看看这些PasswordEncoder的具体实现。

2)使用BCryptPasswordEncoder

1、配置BCryptPasswordEncoder

在安全配置类中定义:

@Bean

public PasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();

}

测试发现认证失败,提示:Encoded password does not look like BCrypt。

原因:

由于UserDetails中存储的是原始密码(比如:123),它不是BCrypt格式。

跟踪 DaoAuthenticationProvider第33行代码查看 userDetails中的内容 ,跟踪第38行代码查看

PasswordEncoder的类型。

授权流程

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

image-20230618145621375

分析授权流程:

  1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子

类拦截。

  1. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类

DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限

Collection<ConfigAttribute> 。

SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则,

取访问策略如:

http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...

  1. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

AccessDecisionManager(访问决策管理器)的核心接口如下:

public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication , Object object, Collection<ConfigAttribute>
configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
//略..
}

这里着重说明一下decide的参数:

authentication:要访问资源的访问者的身份

object:要访问的受保护资源,web请求对应FilterInvocation

confifigAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。

decide**接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。**

授权决策

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源

通过上图可以看出,AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。

AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

public interface AccessDecisionVoter<S> {
int ACCESS_GRANTED = 1;
int ACCESS_ABSTAIN = 0;
int ACCESS_DENIED = ‐1;
boolean supports(ConfigAttribute var1);
boolean supports(Class<?> var1);
int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意,

ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前

Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。

Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是

AffirmativeBasedConsensusBasedUnanimousBased

AffirmativeBased的逻辑是:

(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

(2)如果全部弃权也表示通过;

(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

Spring security默认使用的是AffirmativeBased。

ConsensusBased的逻辑是:

(1)如果赞成票多于反对票则表示通过。

(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。

(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表

示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

(4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值

为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递

给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfifigAttribute给

AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的

ConfifigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。

UnanimousBased的逻辑具体来说是这样的:

(1)如果受保护对象配置的某一个ConfifigAttribute被任意的AccessDecisionVoter反对了,则将抛出

AccessDeniedException。

(2)如果没有反对票,但是有赞成票,则表示通过。

(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,

true则通过,

false则抛出

AccessDeniedException。

Spring Security也内置一些投票者实现类如RoleVoterAuthenticatedVoterWebExpressionVoter等,可以自行查阅资料进行学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值