首页 > 编程学习 > Day238.shiro和SpringSecurity对比、HttpBasicformLogin模式登陆认证模式、自定义登陆验证结果处理等 -springsecurity-jwt-oauth2

1、spring-security简介并与shiro对比

一、 SpringSecurity 框架简介

官网:https://projects.spring.io/spring-security/
源代码: https://github.com/spring-projects/spring-security/
Spring Security 是强大的,且容易定制的,基于Spring开发的实现认证登录与资源授权的应用安全框架。

SpringSecurity 的核心功能:

  • Authentication:身份认证,用户登陆的验证(解决你是谁的问题)
  • Authorization:访问授权,授权系统资源的访问权限(解决你能干什么的问题)
  • 安全防护,防止跨站请求,session 攻击等

二、比较一下shiro与Spring Security

目前在java web应用安全框架中,与Spring Security形成直接竞争的就是shiro,二者在核心功能上几乎差不多,但从使用的角度各有优缺点。笔者认为:没有最好的,只有最合适的。

2.1 用户量

Shiro的使用量一直高于spring security。但是从趋势上来看(2020年10月的百度指数),Spring Security是在一直上升的,shiro的使用量同比、环比都进入了下滑期。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q3FkOTI6-1617534829662)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403191135.png)]


2.2.使用的方便程度

通常来说,shiro入门更加容易,使用起来也非常简单,这也是造成shiro的使用量一直高于Spring Security的主要原因。但是从笔者的角度来看,二者其实都简单,我说说我的理由:

  • 在没有Spring Boot之前,Spring Security的大部分配置要通过XML实现,配置还是还是非常复杂的。但是有了 Spring Boot之后,这一情况已经得到显著改善。
  • Spring Security之所以看上去比shiro更复杂,其实是因为它引入了一些不常用的概念与规则。大家应该都知道2/8法则,这在Spring Security里面体现的特别明显,如果你只学Spring Security最重要的那20%,这20%的复杂度和shiro基本是一致的。也就是说,不重要的那80%,恰恰是Spring Security比shiro的“复杂度”。

也就是说,如果有人能帮你把Spring Security最重要的那20%摘出来,二者的入门门槛、复杂度其实是差不太多的。

2.3.社区支持

Spring Security依托于Spring庞大的社区支持,这点自不必多说。shiro属于apache社区,因为它的广泛使用,文档也非常的全面。二者从社区支持来看,几乎不相上下。

但是从社区发展的角度看,Spring Security明显更占优势,随着Spring Cloud、Spring Boot、Spring Social的长足进步,这种优势会越来越大。因为Spring Security毕竟是Spring的亲儿子,Spring Security未来在于Spring系列框架集成的时候一定会有更好的融合性,前瞻性、兼容性!这也是为什么我们要学Spring Security的主要原因!

2.4.功能丰富性

Spring Security因为它的复杂,所以从功能的丰富性的角度更胜一筹。其中比较典型的如:

  • Spring Security默认含有对OAuth2.0的支持,与Spring Social一起使用完成社交媒体登录也比较方便。shiro在这方面只能靠自己写代码实现。
  • 还有一种普遍说法:Spring Security在网络安全的方面下的功夫更多,但是笔者并未有非常直接的感受,有可能出现安全问题的时候才会感到不够安全的痛。

三、总结

如果你只是想实现一个简单的web应用,shiro更加的轻量级,学习成本也更低。如果您正在开发一个分布式的、微服务的、或者与Spring Cloud系列框架深度集成的项目,笔者还是建议您使用Spring Security。


2、需求分析与基础环境准备

一、需求分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UGMRSd3A-1617534829665)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403192317.png)]

  • login.html登录页面,登录页面访问不受限制
  • 在登录页面登录之后,进入index.html首页(登录验证Authentication)
  • 首页可以看到syslog、sysuer、biz1、biz2四个页面选项
  • 我们希望syslog(日志管理)和sysuser(用户管理)只有admin管理员可以访问(权限管理Authorization)
  • biz1、biz2普通的操作普通用户登录即可访问(权限管理Authorization)

其中 login.html是html文件,其他文件是以.html为后缀的freemarker模板文件

我们先将以上页面准备好,先不做任何访问权限上的限制!以上需求我们将在后面章节实现

二、环境准备

  1. 起一个新的spring boot2.x 版本的web应用
  2. 集成lombok、mybatis、log4j等
  3. 集成一个前端模板,我这里使用的是freemarker。你可以根据自己需要整合jsp、thymeleaf等都可以。

为了让课程尽量的干一点,因为这套课程主要是讲security及web应用安全,所以不会花时间在spring boot整合开源软件,及增删改查怎么写!如果不知道怎么做。可以参考:https://www.kancloud.cn/hanxt/springboot2/content 网上也有很多的资料。

如果你实在不愿意自己集成,下面的是我整合之后的结果,可以直接使用。该项目只用于项目初始化,不包含后面课程的源代码。
https://gitee.com/hanxt/boot-security-starter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5qxGqV7-1617534829670)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403193513.png)]

注意login.html页面的存放位置和其他文件的存放位置不一样。public文件夹里面的html文件可以对外公开访问

  • login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<h1>阿昌业务系统登录</h1>
<form action="/login" method="post">
    <span>用户名称</span><input type="text" name="username" /> <br>
    <span>用户密码</span><input type="password" name="password" /> <br>
    <input type="submit" value="登陆">
</form>

</body>
</html>
  • index.html
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8" />
    <title>阿昌业务管理系统</title>
</head>
<body>

<h1>阿昌业务管理系统</h1>
<br>
<a href="/syslog">日志管理</a>
<br>
<a href="/sysuser">用户管理</a>
<br>
<a href="/biz1">具体业务一</a>
<br>
<a href="/biz2">具体业务二</a>

</body>
</html>
  • application.yml
server:
  port: 8888

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 00000
    driver-class-name: com.mysql.cj.jdbc.Driver
  freemarker:
    cache: false # 缓存配置 开发阶段应该配置为false 因为经常会改
    suffix: .html # 模版后缀名 默认为ftl
    charset: UTF-8 # 文件编码
    template-loader-path: classpath:/templates/ # 模版的路径地址
  • BizpageController
@Controller
public class BizpageController {

    // 登录
    @PostMapping("/login")
    public String index(String username,String password) {
        return "index";
    }

    // 日志管理
    @GetMapping("/syslog")
    public String showOrder() {
        return "syslog";
    }

    // 用户管理
    @GetMapping("/sysuser")
    public String addOrder() {
        return "sysuser";
    }

    // 具体业务一
    @GetMapping("/biz1")
    public String updateOrder() {
        return "biz1";
    }

    // 具体业务二
    @GetMapping("/biz2")
    public String deleteOrder() {
        return "biz2";
    }

}

syslog.html、sysuser.html、biz1.html、biz2.html内容随便写点,能够方便做内容上的彼此区分即可。


3、HttpBasic模式登录认证

一、HttpBasic模式的应用场景

HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验证。

就好像是我小时候写日记,都买一个带小锁头的日记本,实际上这个小锁头有什么用呢?如果真正想看的人用一根钉子都能撬开。它的作用就是:某天你的父母想偷看你的日记,拿出来一看还带把锁,那就算了吧,怪麻烦的。

举一个我使用HttpBasic模式的进行登录验证的例子:我曾经在一个公司担任部门经理期间,开发了一套用于统计效率、分享知识、生成代码、导出报表的Http接口。纯粹是为了工作中提高效率,同时我又有一点点小私心,毕竟各部之间是有竞争的,所以我给这套接口加上了HttpBasic验证。公司里随便一个技术人员,最多只要给上一两个小时,就可以把这个验证破解了。说白了,这个工具的数据不那么重要,加一道锁的目的就是不让它成为公开数据。如果有心人破解了,真想看看这里面的数据,其实也无妨。这就是HttpBasic模式的典型应用场景。

二、spring boot2.0整合Spring security

spring boot 2,x版本maven方式引入Spring security坐标。

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

三、HttpBasic登录认证模式

如果使用的Spring Boot版本为1.X版本,依赖的Security 4.X版本,那么就无需任何配置,启动项目访问则会弹出默认的httpbasic认证.

我们现在使用的是spring boot2.0版本(依赖Security 5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式。所以我们要使用Basic模式,需要自己调整一下。并且security.basic.enabled已经过时了,所以我们需要自己去编码实现。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //进行安全认证及授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()//开启httpbasic认证
        .and()
        .authorizeRequests()//表示所有的请求
        .anyRequest()//匹配规则是所有请求
        .authenticated();//都需要登录认证
        //上面的一顿操作为:所有请求都必须经过springsecurity过滤器链登录认证才能访问,认证模式是httpbasic
    }
    
}

启动项目,在项目后台有这样的一串日志打印,冒号后面的就是默认密码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G6H1r8U6-1617534829676)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403200702.png)]

Using generated security password: d707345c-4d1e-461c-bfe0-865297f8fb11

我们可以通过浏览器进行登录验证,默认的用户名是user.(下面的登录框不是我们开发的,是HttpBasic模式自带的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LHMhRymu-1617534829678)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403200839.png)]

当然我们也可以通过application.yml指定配置用户名密码

spring:
    security:
      user:
        name: admin
        password: admin

四、HttpBasic模式的原理说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BkqUDDC1-1617534829681)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403201549.png)]

  • 首先,HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 "admin" ,密码是“ admin”,则将字符串"admin:admin"使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=。
  • 然后,在Http请求中使用Authorization作为一个Header,“Basic YWtaW46YWRtaW4=“作为Header的值,发送给服务端。(注意这里使用Basic+空格+加密串)
  • 服务器在收到这样的请求时,到达BasicAuthenticationFilter过滤器,将提取“ Authorization”的Header值,并使用用于验证用户身份的相同算法Base64进行解码。
  • 解码结果与登录验证的用户名密码匹配,匹配成功则可以继续过滤器后续的访问。

所以,HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是可逆的,你知道上面的原理,分分钟就破解掉。我们完全可以使用PostMan工具,发送Http请求进行登录验证。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MKaG8yHF-1617534829683)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403201606.png)]

  • 直接拿去base64解密

账号密码直接生成出来!!!!!!

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w298cQlx-1617534829685)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403202126.png)]


4、PasswordEncoder介绍

一、Hash算法加密

Hash算法特别的地方在于它是一种单向算法,用户可以通过hash算法对某个数据生成一段特定长度的唯一hash值,却不能通过这个hash值逆向获取原始数据。因此Hash算法常用在不可还原的密码存储、数据完整性校验等领域。不可逆向解密

那问题来了,密码只能单向加密不能解密,那如何校验密码的正确性?我们来看Spring Security中的接口PasswordEncoder ,并对这个问题进行解答。

二、PasswordEncoder 接口

PasswordEncoder 是Spring Scurity框架内处理密码加密与校验的接口。

package org.springframework.security.crypto.password;

public interface PasswordEncoder {
    //提供未加密的密码,返回被加密的密码的hash值
   String encode(CharSequence rawPassword);

    //判断用户输入的原始密码和上面encode()生成的加密的密码是否匹配
   boolean matches(CharSequence rawPassword, String encodedPassword);

    //判断当前被加密后的密码是否需要升级,也就是说,是否需要重新加密
   default boolean upgradeEncoding(String encodedPassword) {
      return false;
   }
}

这个接口有三个方法

  • encode()方法接受的参数是原始密码字符串,返回值是经过加密之后的hash值,hash值是不能被逆向解密的。这个方法通常在为系统添加用户,或者用户注册的时候使用。
  • matches()方法是用来校验用户输入密码rawPassword,和加密后的hash值encodedPassword是否匹配。如果能够匹配返回true,表示用户输入的密码rawPassword是正确的,反之返回fasle。也就是说虽然这个hash值不能被逆向解密,但是可以判断是否和原始密码匹配。这个方法通常在用户登录的时候进行用户输入密码的正确性校验。
  • upgradeEncoding()设计的用意是,判断当前的密码是否需要升级。也就是是否需要重新加密?需要的话返回true,不需要的话返回fasle。默认实现是返回false。

例如,我们可以通过如下示例代码在进行用户注册的时候加密存储用户密码

user.setPassword(passwordEncoder.encode(user.getPassword()));
//将User保存到数据库表,该表包含password列

三、接口实现类

BCryptPasswordEncoder 是Spring Security推荐使用的PasswordEncoder接口实现类

@Test
void contextLoads() {

    PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    String rawPwd = "123456";
    String encodePwd = passwordEncoder.encode(rawPwd);

    System.out.println("原始密码:"+rawPwd);
    System.out.println("加密后的密码::"+encodePwd);

    System.out.println(rawPwd+"是否匹配"+encodePwd+":" +passwordEncoder.matches(rawPwd,encodePwd));

    System.out.println("654321是否匹配"+encodePwd +":"+passwordEncoder.matches("654321",encodePwd));

}
  • 结果

上面的测试用例执行的结果是下面这样的。(注意:对于同一个原始密码,每次加密之后的hash密码都是不一样的,这正是BCryptPasswordEncoder的强大之处,它不仅不能被破解,想通过常用密码对照表进行大海捞针你都无从下手)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EF5GN9Sd-1617534829688)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403203937.png)]

BCrypt产生随机盐(盐的作用就是每次做出来的菜味道都不一样)。这一点很重要,因为这意味着每次encode将产生不同的结果。

$2a$10$zt6dUMTjNSyzINTGyiAgluna3mPm7qdgl26vj4tFpsFO6WlK5lXNm

BCrypt加密后的密码有三个部分,由 $分隔:

  1. "2a"表示 BCrypt 算法版本
  2. "10"表示算法的强度
  3. "zt6dUMTjNSyzINTGyiAglu"部分实际上是随机生成的盐。通常来说前 22 个字符是盐,剩余部分是纯文本的实际哈希值。
  4. “na3mPm7qdgl26vj4tFpsFO6WlK5lXNm” 文本的hash值

虽然每次生成的hash值都不一样,但是只要保证原始密码正确,再进行bcrypt加密,数据库储存的加密后的密码还是可以通过matches()方法进行匹配判断是否匹配

BCrypt*算法生成长度为 60 的字符串,因此我们需要确保密码将存储在可以容纳密码的数据库列中。


5、formLogin模式登录认证

一、formLogin的应用场景

在本专栏之前的文章中,已经给大家介绍过Spring Security的HttpBasic模式,该模式比较简单,只是进行了通过携带Http的Header进行简单的登录验证,而且没有可以定制的登录页面,所以使用场景比较窄。
对于一个完整的应用系统,与登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面,也就是本文给大家介绍的formLogin模式登录认证模式。

需要注意的是:有的朋友会被Form Login这个名字误解,Form Login不是只有使用html中的form 表单才能实现登录功能,使用js发起登录请求也是可以的

准备工作

  • 参考上面的《需求分析与基础环境准备》
  • 将《Http Basic模式登录认证》的配置内容从项目里面删掉
  • 仍然需要在项目里面通过maven引入如下坐标
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • 把下面的代码从BizpageController里面删掉。这涉及到一个非常重要的问题,就是Spring Security的登录认证并不需要我们自己去写登录认证的Controller方法,而是使用过滤器UsernamePasswordAuthenticationFilter(下一节会源码分析),这个过滤器是·默认集成的·,所以并不需要我们自己去实现登录认证逻辑。我们实现登录功能只需要做配置就可以了,所以把下面的代码从项目里面删掉。
// 登录
@PostMapping("/login")
public String index(String username,String password) {
    return "index";
}

二、说明

formLogin登录认证模式的三要素:

  • 登录认证逻辑-登录URL、如何接收登录参数、登陆成功后逻辑(静态)
  • 资源访问控制规则-决定什么用户、什么角色可以访问什么资源(动态-数据库)
  • 用户具有角色权限-配置某个用户拥有什么角色、拥有什么权限(动态-数据库)

一般来说,使用权限认证框架的的业务系统登录验证逻辑是固定的,而资源访问控制规则和用户信息是从数据库或其他存储介质灵活加载的。但本文所有的用户、资源、权限信息都是代码配置写死的,旨在为大家介绍formLogin认证模式,如何从数据库加载权限认证相关信息我还会结合RBAC权限模型再写文章的。

三、登录认证及资源访问权限的控制

首先,我们要继承WebSecurityConfigurerAdapter ,重写configure(HttpSecurity http) 方法,该方法用来配置登录验证逻辑。请注意看下文代码中的注释信息。

//登录认证及资源访问权限的控制
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()//关闭csrf防御
        .formLogin()//开启formLogin认证
        .loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
        .loginProcessingUrl("/login")//登录表单form中action的地址,也就是处理认证请求的路径
        .usernameParameter("username")//登录表单form中用户名输入框input的name名,不修改的话默认是username
        .passwordParameter("password")//form中密码输入框input的name名,不修改的话默认是password
        .defaultSuccessUrl("/")//如果用户输入了正确的账号密码,默认转跳的路径
        .and()
        .authorizeRequests()
        .antMatchers("/login.html","/login").permitAll()//不需要通过验证就可以被访问的资源地址,permitAll()允许访问的方法
        .antMatchers("/","/biz1","biz2")//资源路径配置
        //hasAnyAuthority():有权限
        .hasAnyAuthority("ROLE_user","ROLE_admin")//只要你是user角色和admin角色【其中之一】就可以访问:"/","/biz1","biz2",指定都可以访问的角色
        .antMatchers("/syslog","/sysuser")//资源路径配置
        //hasAnyRole():有角色
        .hasAnyRole("admin")//只要你是admin角色可以访问:"/syslog","/sysuser"
        //.antMatchers("/syslog").hasAuthority("sys:log")
        //.antMatchers("/sysuser").hasAuthority("sys:user") 访问("/syslog","/sysuser")请求需要携带("sys:log","sys:user")权限id ①①①①①①①

        //hasAnyAuthority("ROLE_user","ROLE_admin") = hasAnyRole("admin") 的写法
        //hasAnyAuthority只要在方法中加上ROLE_XXX 就等价于hasAnyRole("XXXX")的写法
        .anyRequest().authenticated();
}

上面的代码分为两部分:

  • 第一部分是formLogin配置段,用于配置登录验证逻辑相关的信息。如:登录页面、登录成功页面、登录请求处理路径等。和login.html页面的元素配置要一一对应。
  • "/"在spring boot应用里面作为资源访问的时候比较特殊,它就是“/index.html”.所以defaultSuccessUrl登录成功之后就跳转到index.html

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ec6cjJsW-1617534829689)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403213223.png)]

  • 第二部分是authorizeRequests配置段,用于配置资源的访问控制规则。如:开发登录页面的permitAll开放访问,“/biz1”(业务一页面资源)需要有角色为user或admin的用户才可以访问。

  • hasAnyAuthority("ROLE_user","ROLE_admin")等价于hasAnyRole("user","admin"),角色是一种特殊的权限。

    //hasAnyAuthority("ROLE_user","ROLE_admin") = hasAnyRole("admin") 的写法
    //hasAnyAuthority只要在方法中加上ROLE_XXX 就等价于hasAnyRole("XXXX")的写法
    
  • "sys:log"或"sys:user"是我们自定义的权限ID,有这个ID的用户可以访问对应的资源

这时候我们通过浏览器访问,随便测试一个用户没有访问权限的资源,都会跳转到login.html页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLSkm3a3-1617534829691)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403213414.png)]


四、用户及角色信息配置

在上文中,我们配置了登录验证及资源访问的权限规则,我们还没有具体的用户,下面我们就来配置具体的用户。重写WebSecurityConfigurerAdapter的 configure(AuthenticationManagerBuilder auth)方法

  • 下面配置了两个用户
    • 用户名:密码:角色
      • 访问权限角色是上面设置的,这里设置的是用户和对应的角色是什么
      • user:123456:user(有权访问"/","/biz1",“biz2”)
      • admin:123456:admin(有权访问"/syslog","/sysuser","/","/biz1",“biz2”)
//用户及角色信息配置
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()//存储在内存中的登录认证方式
        .withUser("user")
        .password(passwordEncoder().encode("123456"))
        .roles("user")
        .and()
        .withUser("admin")
        .password(passwordEncoder().encode("123456"))
        //.authorities("sys:log","sys:user") //配置权限id("sys:log","sys:user")对应上面的①①①①①
        .roles("admin")
        .and()
        .passwordEncoder(passwordEncoder());//配置使用BCrypt加密
}

//注入BCrypt加密
@Bean
public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
  • inMemoryAuthentication指的是在内存里面存储用户的身份认证和授权信息。
  • withUser("user")用户名是user
  • password(passwordEncoder().encode("123456"))密码是加密之后的123456
  • authorities("sys:log","sys:user")指的是admin用户拥有资源ID对应的资源访问的的权限:"/syslog"和"/sysuser"
  • roles()方法用于指定用户的角色,一个用户可以有多个角色

五、静态资源访问

在我们的实际开发中,登录页面login.html和控制层Controller登录验证’/login’都必须无条件的开放。除此之外,一些静态资源如css、js文件通常也都不需要验证权限,我们需要将它们的访问权限也开放出来。下面就是实现的方法:重写WebSecurityConfigurerAdapter类的configure(WebSecurity web) 方法

@Override
public void configure(WebSecurity web) {
    //将项目中静态资源路径[开放]出来
    web.ignoring().antMatchers( "/css/**", "/fonts/**", "/img/**", "/js/**");
}

那么这些静态资源的开放,和Controller服务资源的开放为什么要分开配置?有什么区别呢?

  • Controller服务资源要经过一系列的过滤器的验证,我们配置的是验证的放行规则
  • 这里配置的是静态资源的开放,不经过任何的过滤器链验证,直接访问资源

六、一个奇怪的问题

有朋友问:我们的login登录认证是通过配置实现的,我们没有写Controller,也只接收了username和password两个参数,那如果我们在登陆的时候有更多的参数需要接收该怎么办?

刚开始有朋友问我这个问题的时候,我真的感觉这个问题很奇怪,如果你不用spring security,该怎么获取参数?当然从Http Request里面去获取啊,怎么到了Spring security里面就不会了呢?后来问的同学越来越多,我发现这还真是一个普遍问题:不写Controller就不会传参了

统一答复:该怎么获取参数,哪里有HttpServletRequest,那里就能获取参数。比如:

  • 自定义一个Filter过滤器,过滤器里面肯定有HttpServletRequest吧。当然我们一般不在自定义过滤器里面去做实际业务处理,所以这种方法暂不考虑。
  • 另外就是我们后面章节为大家介绍的 《自定义登录验证结果处理》,在登录成功之后我们可以实现一定的业务逻辑(建议用这种) 。在这里面可以获取参数,想传多少参数就传多少。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssnUqScO-1617534829692)(http://qpgf4uqra.hn-bkt.clouddn.com/20210403223056.png)]

其实归根结底还是大家对于HTTP协议不清楚、Spring基础不扎实!HTTP 请求头、请求体、queryString等等很多传参渠道,Spring的过滤器、拦截器很多都可以HttpServletRequest拦截请求。


6、源码解析登录验证流程

一、登录认证基于过滤器链

Spring Security的登录验证流程核心就是过滤器链。

image-20210404121449408

  • 贯穿于整个过滤器链始终有一个上下文对象SecurityContext和一个Authentication对象(登录认证的主体)
  • 一旦某一个该主体通过其中某一个过滤器的认证,Authentication对象信息被填充,比如:isAuthenticated=true表示该主体通过验证。
  • 如果该主体通过了所有的过滤器,仍然没有被认证,在整个过滤器链的最后方有一个FilterSecurityInterceptor过滤器(虽然叫Interceptor,但它是名副其实的过滤器,不是拦截器)。判断Authentication对象的认证状态,如果没有通过认证则抛出异常,通过认证则访问后端API。
  • 之后进入响应阶段,FilterSecurityInterceptor抛出的异常被ExceptionTransactionFilter对异常进行相应的处理。比如:用户名密码登录异常,会被引导到登录页重新登陆。
  • 如果是登陆成功且没有任何异常,在请求响应中最后一个过滤器SecurityContextPersistenceFilter中将SecurityContext放入session。下次再进行请求的时候,直接从SecurityContextPersistenceFilter的session中取出认证信息。从而避免多次重复认证。

SpringSecurity提供了多种登录认证的方式,由多种Filter过滤器来实现,比如:

  • BasicAuthenticationFilter实现的是HttpBasic模式的登录认证
  • UsernamePasswordAuthenticationFilter实现用户名密码的登录认证
  • RememberMeAuthenticationFilter实现登录认证的“记住我”的功能
  • SocialAuthenticationFilter实现社交媒体方式登录认证的处理,如:QQ、微信
  • Oauth2AuthenticationProcessingFilter和Oauth2ClientAuthenticationProcessingFilter实现Oauth2的鉴权方式

根据我们不同的需求实现及配置,不同的Filter会被加载到应用中。


二、过滤器登录验证细节

image-20210404121708812

2.1.构建登录认证主体

如图所示,当用户登陆的时候首先被某一种认证方式的过滤器拦截(以用户名密码登录为例)。如:UsernamePasswordAuthenticationFilter会使用用户名和密码创建一个登录认证凭证:UsernamePasswordAuthenticationToken,进而获取一个Authentication对象,该对象代表身份验证的主体,贯穿于用户认证流程始终。

image-20210404130534577


2.2.多种认证方式的管理 ProviderManager

随后使用AuthenticationManager 接口对登录认证主体进行authenticate认证。

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throwsAuthenticationException;
}

ProviderManager继承于AuthenticationManager是登录验证的核心类。

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    ……
    private List<AuthenticationProvider> providers;
    ……

ProviderManager保管了多个AuthenticationProvider,每一种登录认证方式都可以尝试对登录认证主体进行认证。只要有一种方式被认证成功,Authentication对象就成为被认可的主体。

public interface AuthenticationProvider {
    Authentication authenticate(Authentication var1) throws AuthenticationException;
    boolean supports(Class<?> var1);
}
  • RememberMeAuthenticationProvider定义了“记住我”功能的登录验证逻辑
  • DaoAuthenticationProvider加载数据库用户信息,进行用户密码的登录验证

image-20210404130619505


2.3 数据库加载用户信息 DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

从数据库获取用户信息

image-20210404130641129

所以当我们需要加载用户信息进行登录验证的时候,我们需要实现UserDetailsService接口,重写loadUserByUsername方法,参数是用户输入的用户名。返回值是UserDetails

2.4.SecurityContext

完成登录认证之后,将认证完成的Authtication对象(authenticate: true, 有授权列表authority list, 和username信息)放入SecurityContext上下文里面。后续的请求就直接从SecurityContextFilter中获得认证主体,从而访问资源。


三、结合源码讲解登录验证流程

我们就以用户名、密码登录方式为例讲解一下Spring Security的登录认证流程。

image-20210404130659454

3.1 UsernamePasswordAuthenticationFilter

该过滤器封装用户基本信息(用户名、密码),定义登录表单数据接收相关的信息。如:

  • 默认的表单用户名密码input框name是username、password
  • 默认的处理登录请求路径是/login、使用POST方法

image-20210404130712957

image-20210404130720265

3.2 AbstractAuthenticationProcessingFilter的doFilter方法的验证过程

UsernamePasswordAuthenticationFilter继承自抽象类AbstractAuthenticationProcessingFilter,该抽象类定义了验证成功与验证失败的处理方法。

image-20210404130731908

3.3 验证成功之后的Handler和验证失败之后的handler

AbstractAuthenticationProcessingFilter中定义了验证成功与验证失败的处理Handler。

private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

也就是说当我们需要自定义验证成功或失败的处理方法时,要去实现AuthenticationSuccessHandler或AuthenticationfailureHandler接口

image-20210404130747060


7、自定义登录验证结果处理

一、需要自定义登录结果的场景

在我之前的文章中,做过登录验证流程的源码解析。其中比较重要的就是

http.formLogin()
        .loginPage("/login.html")//一旦用户的请求没有权限就跳转到这个页面
        .defaultSuccessUrl("/")//登录认证成功后默认转跳的路径
        .failureUrl("/login.html")//登陆失败跳转路径
  • 当我们登录成功的时候,是由AuthenticationSuccessHandler进行登录结果处理,默认跳转到defaultSuccessUrl配置的路径对应的资源页面(一般是首页index.html)。
  • 当我们登录失败的时候,是由AuthenticationfailureHandler进行登录结果处理,默认跳转到failureUrl配置的路径对应的资源页面(一般也是跳转登录页login.html,重新登录)。

但是在web应用开发过程中需求是千变万化的,有时需要我们针对登录结果做个性化处理,比如:

  • 我们希望不同的人登陆之后,看到不同的首页(及向不同的路径跳转)
  • 我们应用是前后端分离的,验证响应结果是JSON格式数据,而不是页面跳转
  • …… 其他未尽的例子

以上的这些情况,使用Spring Security作为安全框架的时候,都需要我们使用本节学到的知识进行自定义的登录验证结果处理。


二、自定义登陆成功的结果处理

为了满足上面的需求,我们该如何去做呢?下面一小节我们来说明一下。

AuthenticationSuccessHandler接口是Security提供的认证成功处理器接口,我们只需要去实现它即可。

但是通常来说,我们不会直接去实现AuthenticationSuccessHandler接口,而是继承SavedRequestAwareAuthenticationSuccessHandler 类,这个类会记住用户上一次请求的资源路径

比如:用户请求books.html,没有登陆所以被拦截到了登录页,当你万成登陆之后会自动跳转到books.html,而不是主页面。

image-20210404155609076

  • application.yaml

配置了一个自定义的属性

spring:
  security:
    logintype: JSON
  • 自定义登陆成功的结果处理
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    //在application配置文件中配置登陆的类型是JSON数据响应还是做页面响应
    @Value("${spring.security.logintype}")
    private String loginType;

    //Jackson JSON数据处理类:将类转换为json对象
    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
        //判断当前登录认证的loginType为JSON
        if (loginType.equalsIgnoreCase("JSON")){
            //如果登入认证格式为JSON格式
            response.setContentType("application/json;charset=UTF-8");//指定响应的数据类型为JSON格式
            response.getWriter().write(objectMapper.writeValueAsString(AjaxResponse.success()));//写会XXX的JSON数据
        }else {
            //如果登入认证格式不为JSON格式
            //就跳转到用户请求的上一个页面,【SavedRequestAwareAuthenticationSuccessHandler】已经帮我实现这个操作了,直接调用父类即可实现
            super.onAuthenticationSuccess(request,response,authentication);
        }
    }

}
  • 在上面的自定义登陆成功处理中,既适应JSON前后端分离的应用登录结果处理,也适用于模板页面跳转应用的登录结果处理
  • ObjectMapper 是Spring Boot默认集成的JSON数据处理类库Jackson中的类。
  • AjaxResponse是一个自定义的通用的JSON数据接口响应类。

三、自定义登录失败的结果处理

这里我们同样没有直接实现AuthenticationFailureHandler接口,而是继承SimpleUrlAuthenticationFailureHandler类。该类中默认实现了登录验证失败的跳转逻辑,即登陆失败之后回到登录页面。我们可以利用这一点简化我们的代码。

@Component
public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

   @Value("${spring.security.logintype}")
   private String loginType;

    //Jackson JSON数据处理类:将类转换为json对象
    private  static ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        if (loginType.equalsIgnoreCase("JSON")){
            //认证的是json的数据结构
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(
                    objectMapper.writeValueAsString(
                            AjaxResponse.error(
                                    new CustomException(
                                            CustomExceptionType.USER_INPUT_ERROR,
                                            "用户名或密码存在错误,请检查后再次登录"))));
        }else {
            //认证的不是json的数据结构
            response.setContentType("text/html;charset=UTF-8");
            //【SimpleUrlAuthenticationFailureHandler】已经实现登陆失败之后回到登录页面
            super.onAuthenticationFailure(request,response,exception);
        }
    }

}
  • 在上面的自定义登陆失败处理中,既适应JSON前后端分离的应用登录失败结果处理,也适用于模板页面跳转应用的登录失败结果处理
  • 登陆失败之后,将默认跳转到默认的failureUrl,即登录界面。

上面自定义完成功处理器和失败处理器后,需要在springsecurity中配置将他们使用起来才会生效


四、配置SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    //登录认证及资源访问权限的控制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
        .formLogin()
                .loginPage("/login.html")
                .loginProcessingUrl("/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(myAuthenticationSuccessHandler)//使用自定义的成功失败处理器
                .failureHandler(myAuthenticationFailureHandler)
        .............

image-20210404165820465

  • 将自定义的AuthenticationSuccessHandler和AuthenticationFailureHandler注入到Spring Security配置类中
  • 使用fromlogin模式,配置successHandler和failureHandler。
  • 不要配置defaultSuccessUrl和failureUrl,否则自定义handler将失效。handler配置与URL配置只能二选一

五、JSON登录方式的测试

最后可以用下面的代码测试一下登录验证的结果。

  • 前端改造

image-20210404170323750

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
</head>
<body>
<h1>阿昌业务系统登录</h1>
<form action="/login" method="post">
    <span>用户名称</span><input type="text" name="username" id="username" /> <br>
    <span>用户密码</span><input type="password" name="password" id="password" /> <br>
    <input type="button" onclick="login()" value="登陆">
</form>

<script type="text/javascript">
    function login() {
        var username = $("#username").val();
        var password = $("#password").val();
        if (username===""||password===""){
            alert("账号密码不能空")
            return
        }
        $.ajax({
            type:"POST",
            url:"/login",
            data:{
                "username":username, //这里的参数名称要和Spring Security配置一致
                "password":password
            },
            success:function (json) {
                if(json.isok){
                    location.href = '/'; //index.html
                }else{
                    alert(json.message);
                    location.href = '/login.html'; //index.html
                }
            },
            error:function (error) {

            }
        })
    }
</script>

</body>
</html>

六、自定义权限访问异常结果处理

除了登陆成功、登陆失败的结果处理,Spring Security还未我们提供了其他的结果处理类。比如用户未登录就访问系统资源,可以实现AuthenticationEntryPoint 接口进行响应处理,提示用户应该先去登录

  • 如果用户没有登录就访问地址的话就会执行这个
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("请先登录再访问");
    }
}

比如用户登录后访问没有权限访问的资源,可以实现AccessDeniedHandler 接口进行相应处理,提示用户没有访问权限

  • 如果用户没有权限访问的话
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("你没有权限访问");
    }
}

通过下面的方法进行注册生效

image-20210404172304205

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    private  MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    //登录认证及资源访问权限的控制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .authenticationEntryPoint(myAuthenticationEntryPoint)
        .and()
            ...............
            
    }       
}

测试访问


  • 如果登录上面的【user:123456:user】用户到/syslog中

image-20210404172530559

  • 如果不登录直接访问/syslog

image-20210404172706385


8、session会话的管理

一、Spring Security创建使用session的方法

Spring Security提供4种方式精确的控制会话的创建:

  • always:如果当前请求没有对应的session存在,Spring Security创建一个session。
  • ifRequired(默认): Spring Security在需要使用到session时才创建session
  • never: Spring Security将永远不会主动创建session,但是如果session在当前应用中已经存在,它将使用该session
  • stateless:Spring Security不会创建或使用任何session。适合于接口型的无状态应用(前后端分离无状态应用),这种方式节省内存资源

image-20210404173233897

在Spring Security配置中加入session创建的策略。继承WebSecurityConfigurerAdapter ,重写configure(HttpSecurity http) 方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement()
        .sessionCreationPolicy(
                SessionCreationPolicy.IF_REQUIRED
        )
}

一般不建议修改

image-20210404173427148

重要的是:该配置只能控制Spring Security如何创建与使用session,而不是控制整个应用程序。如果我们不明确指定,Spring Security可能不会创建session,但是我们的应用程序可能会创建session(一般spring应用的session管理交由Spring Session进行)!


二、会话超时管理

2.1.session会话超时时间配置

在Spring boot应用中有两种设置会话超时时间的方式,Spring Security对这两种方式完全兼容,即:当会话超时之后用户需要重新登录才能访问应用:

  • server.servlet.session.timeout=15m
  • spring.session.timeout = 15m

第一种方式是springBoot应用自带的session超时配置,第二种方式是我们使用Spring Session之后,提供的session超时配置。第二种方式的优先级更高。

注意:在Spring Boot中Session超时最短的时间是一分钟,当你的设置小于一分钟的时候,默认为一分钟。

2.2 会话超时处理

会话超时之后,我们通常希望应用跳转到一个指定的URL,显示会话超时信息。可以使用如下的配置的代码实现。

http.sessionManagement()
    .invalidSessionUrl("/invalidSession.html");    //指定如果session超时后,跳转的页面

以上路径需要配置permitAll()权限,即无需授权即可访问。


三、Spring Security的会话固化保护

session-fixation-protection 即session的固化保护功能,该功能的目的是一定程度上防止非法用户窃取用户session及cookies信息,进而模拟session的行为。
默认情况下,Spring Security启用了migrationSession保护方式。即对于同一个cookies的SESSIONID用户,每次登录验证将创建一个新的HTTP Session会话,旧的HTTP Session会话将无效,并且旧会话的属性将被复制。
说白了,migrationSession保护方式就是每次登录都更换sessionid,实际也是新建了session。

image-20210404181932435

    http.sessionManagement().sessionFixation().migrateSession()
        //默认行为,每次登录之后都会复制之前的内容,并更新一个新的sessionId

如果这不是您需要的方式,则可以使用其他两个选项:

  • 设置为“none”时,原始会话不会无效
  • 设置“newSession”后,将创建一个干净的会话,而不会复制旧会话中的任何属性

四、Cookie的安全

熟悉Session实现原理的朋友一定都知道,提高Cookies的安全性,实际上就是提高session的安全性。在Spring Boot中可以通过配置方式来实现:

server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
  • httpOnly:如果为true,则浏览器脚本将无法访问cookie
  • secure:如果为true,则仅通过HTTPS连接发送cookie,HTTP无法携带cookie。

9、同账号多端登录踢下线

同账号多端登陆,先登陆的账号被踢下线的功能

一、实现需求

上一节中虽然固化保护的策略可以一定程度保护session复制、窃取,但是在我们绝大部分的应用需求中,都会限制一个用户只能占用一个session。就像我们经常使用QQ,用户在别的地方登录,之前的登陆就会下线。使用Spring Security的配置我们可以轻松的实现这个功能。

.sessionManagement()
    .maximumSessions(1)//设置同一个用户最大的登录客户端(浏览器)数量为1
    .maxSessionsPreventsLogin(false)//设置为false,表示允许再次登录,但先登录的用户会被踢下线
    .expiredSessionStrategy(new CustomExpiredSessionStrategy())
    //一个用户session超时或被踢下线后的自定义操作
  • maximumSessions表示同一个用户最大的登录数量
  • maxSessionsPreventsLogin提供两种session保护策略:
    • true表示已经登录就不予许再次登录,
    • false表示允许再次登录但是之前的登录账户会被踢下线
  • expiredSessionStrategy表示自定义一个session被下线(超时)之后的处理策略。

1.1.跳转到指定页面

通过实现SessionInformationExpiredStrategy 接口来自定义session被下线(超时)之后的处理策略。可以跳转到某一个url对应的HTML页面上,这个页面给用户相对有好的提示:您的登录认证已经过期,或者您在另外的设备上进行登录,这里被迫下线。

image-20210404183430659

public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {

    //页面跳转类
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    //当发现session超时,或session被踢下线后会执行的方法
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        //参数1:request
        //参数2:response
        //参数3:指定跳转的页面
        redirectStrategy.sendRedirect(event.getRequest(),event.getResponse(),"某个url");
    }
}

1.2.Json的友好数据提示

如果你开发的是前后端分离的应用,使用JSON进行数据交互,可以使用如下代码。

public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {

    //jackson的JSON处理对象
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HashMap<String, Object> map = new HashMap<>();
        map.put("code",403);
        map.put("msg","你的登录已超时或登录在另一台机器上,你被迫下线"+event.getSessionInformation().getLastRequest());

        //map -> json
        String json = objectMapper.writeValueAsString(map);
        
        //输出JSON信息的数据
        event.getResponse().setContentType("application/json;charset=UTF-8");
        event.getResponse().getWriter().write(json);

    }

}

image-20210404190737657

二、测试方法:

  • 设置maxSessionsPreventsLogin为false,打开两个浏览器、一个先登录、一个后登录。 然后随便访问一个先登录的浏览器中的应用页面,比如:“用户管理”,显示:“您的登录已经超时或者已经在另一台机器登录,您被迫下线”。表示我们的配置正确。
  • 然后关闭浏览器再次测试,设置maxSessionsPreventsLogin为true,重启应用。先在一个浏览器登录的用户可以登陆,但是另一个浏览器再次登陆就无法登陆,会被拒绝。
Copyright © 2010-2022 ngui.cc 版权所有 |关于我们| 联系方式| 豫B2-20100000