SpringSecurity1: spring boot web 样例快速体验

本文只讲操作实践,不讲原理,这样对于想快速搭建起一个基于SpringSecurity的Web项目的朋友们而言,比较友好。文章主要由两部分构成:

  • 快速演示样例
    所有账户和授权数据均基于内存,能在极短的时间内搭建和运行起来,可以快速体验SpringSecurity

  • 简易生产样例
    建议想参考原型,在自己工程中引入Spring Security的读者,直接跳到这一章节阅读。
    这个样例工程将账户和权限数据存储于数据库中,同时授权清单也不再是硬编码在代码里的了,这样更接近于实际项目中的做法。

一、快速演示样例

本样例所有账户和授权数据均位于内存中,有两个受保护的资源页面,分别是书籍列表和系统信息。前者仅要求登录即可访问,后者则要求登录者具有Admin角色才可以访问,详细资源清单及权限要求如下:

uri 匿名用户 user用户 admin用户 资源说明
/ 或 /index ✔️ ✔️ ✔️ 首页
/login ✔️ ✔️ ✔️ 登录页面
/logout ✔️ ✔️ ✔️ 退出页面
/books ✔️ ✔️ 书籍列表页面
/system ✔️ 系统信息页面

1.1 效果演示

SpringSecurity快速体验样例截屏动图

演示操作流程为:

  1. 进入首页http://locahost:8080或http://locahost:8080/index
  2. 点击[Book List]链接,由于没有登录,会进入登录页面
  3. 在登录页面输入用户名user和密码codefate,点击Login,此后便进入了BookList页面。
  4. 在Book List页面点击[Back Home]重新进入首页
  5. 在首页点击[System Info]链接,虽然当前已登录,但角色为USER,不是ADMIN,因此无没有权限,会进入一个403页面。该页面由spring web框架生成,可以自定义。
  6. 回退到首页,点击[SignOut]退出登录,重新进入登录界面,(也可以点击[SignIn]在不退出的情况下,直接进入登录页面)
  7. 在登录页面输入用户名admin和密码codefate后,回到首页
  8. 在首页点击[System Info]链接,可成功进入
  9. 回到首页,点击[Book List]链接,可成功进入,可见ADMIN角色的用户同样可以访问,反之则不行。

📣 特别说明 ①

  1. 开启VPN运行

    下载本Demo源码后,可在本机编译运行,但登录页面打开非常缓慢,原因是这个security内置的登录页面,会去外网下载以下两个css

    • signin.css
    • bootstrap.min.css

    由于资源在国外,所以下载很慢,最终表现为页面打开缓慢,如你有VPN则建议打开,就能体验到上面gif动图的流畅。

  2. 使用 mvn spring-boot:run 命令启动

    如果您按照 springboot 工程的标准启动方式运行起来后,访问 jsp 页面出现了404的错误,可以像下图这样,利用IDE的maven工具,通过spring-boot插件的run阶段来启动:

    在IntelliJIdea中启动有Jsp的spring-boot-web工程

1.2 项目结构

本样例涉及的依赖及环境信息如下:
jdk1.8、spring-boot-2.7.10、spring-webmvc-5.3.26、jstl-1.2、spring-security-5.7.7、maven-3.8.7、intellij-idea-2022.2、windows11

项目结构如下:

快速体验Demo工程的代码结构

1.3 引入Web依赖

下面是pom.xml的内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>guzb.cnblogs</groupId>
    <artifactId>spring-security-quick-demo</artifactId>
    <version>1.0.0</version>
    <description>Spring Security快速体验样例工程</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>

        <!-- 引入下面这两个依赖,方可在spring-boot环境下渲染jsp视图 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>

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

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.7.10</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

1.3 创建Controller和视图

整个工程仅一个HomeControler.java,内容很简单,如下:

package guzb.cnblogs.security.quickdemo.controller;

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

@Controller
public class HomeController {

    @GetMapping(path={"/", "/index"})
    public String index() {
        return "index";
    }

    @GetMapping(path="/books")
    public String listBooks() {
        return "books";
    }

    @GetMapping(path="/system")
    public String systemInfo() {
        return "system";
    }
}

HomeController中,只是对3个jsp的转发,3个jsp内容如下:

index.jsp
<%@ page language="java" contentType="text/html;charset=utf-8" %>
<html>
<head>
    <title>Home - Spring Security Quick Explorer</title>
</head>
<body>
    <h2> Hi, Welcome to Spring Security Quick Explore Project ! </h2>

    <div>
        <a href="/login">Sign In</a> &nbsp;
        <a href="/logout">Sign Out</a>
        |
        <a href="/books">Book List</a> &nbsp;
        <a href="/system">System Info</a>
    </div>
</body>
</html>
books.jsp
<%@ page language="java" contentType="text/html;charset=utf-8" %>
<html>
<head>
    <title>Book List - Spring Security Quick Explorer</title>
</head>
<body>
    <div>
        <h2> Hi, Welcome to Spring Security Quick Explore Project ! </h2>
        <ul>
            <li>《出埃及记》</li>
            <li>《明朝哪些事》</li>
            <li>《盐亭县志》</li>
            <li>《已亥杂诗》</li>
            <li>《纪念终将逝去的青春》</li>
        </ul>
    </div>

    <div>
        <a href="/">Back Home</a>
    </div>
</body>
</html>
system.jsp
<%@ page language="java" contentType="text/html;charset=utf-8" %>
<html>
<head>
    <title>System Info - Spring Security Quick Explorer</title>
</head>
<body>
    <div>
        <h2> SYSTEM INFORMATION PAGE </h2>
        <p style="max-width: 500px; color: #888;">
            Content of this page are secret, thus,
            It permit super user that has advance privilege,
            example the user with role of Amin, to access only for security reason
        <p>
        <ul>
            <li>System: Windows 11 Pro</li>
            <li>Version: 21H2</li>
            <li>Experience: Windows Feature Experience Pack 1000.22000.1335.0</li>
            <lli>System Type: 64-bit operating system, x64-based processor</li>
            <li>Processor: Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz 2.50 GHz</li>
        </ul>
    </div>

    <div>
        <a href="/">Back Home</a>
    </div>
</body>
</html>

至此,再引入启动类(过于简单,不再贴出源码)后,便是一个简单而完整的web项目了。可以先将pom.xml中的security依赖注释掉,把工程启动起来,检查一下3个页面是否可以正常访问和渲染。启动时记得按照 “特别说明①“ 处的提醒,避免在非重点问题上浪费时间。

1.4 引入SpringSecurity依赖

spring security的maven依赖如下:

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

这个依赖在前面的pom.xml文件已经存在了。

1.5 SpringSecurity 配置

1.5.1 HttpSecurity配置

加入 securit y的 maven 依赖后,默认所有页面都需要认证后才能访问。为了达到前面表格中规划的URI资源访问要求,配置类为 WebSecuritySetting.java,主要内容如下:

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeHttpRequests((auth) -> {
                    auth.antMatchers("/", "/index").permitAll()         // / 和 /index 可匿名访问
                            .antMatchers("/books").authenticated()      // /books 需要登录后才能访问
                            .antMatchers("/system").hasRole("ADMIN");   // /system 不仅需要登录,还要求登录者拥有 ADMIN 角色才能访问
                })
                .formLogin(withDefaults()).httpBasic()                  // 使用sping security内置的登录页面
                .and()
                .logout().deleteCookies("remove")                       // 配置登出后的行为,这是删除cookie
                .invalidateHttpSession(true).logoutUrl("/logout");
        return httpSecurity.build();
    }

以上代码是Spring目前(2023.06)推荐的配置方式,因为它与当前以spring编码方式来配置容器组件(相对于传统的xml方式)很契合。

在这之前,多数项目都是通过继承WebSecurityConfigurerAdapter这个类来配置spring security的。就像下面这样:

基于Adapter方式的配置
@Configuration
@EnableWebSecurity
public class CustomerSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/books/**").authenticated()
            .antMatchers("/system/**").hasRole("ADMIN")
            .anyRequest().permitAll()
            .and()
            .formLogin().and()
            .httpBasic();
     }
}

由于jdk8的接口引入了default方法,传统的Adapter模式就已经失去意义了,之前Spring Framework本身,也有很多的Adpater类,如今它们都贴上了@deprecated标签。因此建议大家也都不要再使用这种方式来定制逻辑或配置了。

1.5.2 密码加密和用户信息

上面的配置指定哪些 URI 资源可以被什么样的用户访问,但当前整个工程还没有用户数据。Spring Security 将用户数据结构抽象为 UserDetail ,同时还抽象出了获取这个 UserDetail 实例的服务类 UserDetailService 。工程中需要提供一个 UserDetailService 的实例,整个项目就基本成型了,就像下面这样:

    @Bean
    public UserDetailsService userDetailsService() {
        List<UserDetails> users = new ArrayList<>(2);
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("user")
                .password("codefate")
                .roles("USER")
                .build();

        users.add(user);

        user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("codefate")
                .roles("ADMIN")
                .build();
        users.add(user);

        return new InMemoryUserDetailsManager(users);
    }

示例中,提供的是基于内存的 UserDetailService 实现类:InMemoryUserDetailsManager。初始化了两个用户,分别是 user 和 admin。

另外,还应当提供一个密码加密器,用于将传递过来的密码明文,加密码后再与用户数据库中保存的加密后的密码文本进行比对校验。本示例提供的是框架默认的委托式加密器,该加密机内部没有任何加密算法,而是将加密逻辑委托给了内置的其它具体加密器,如下所示:

    @Bean
    public PasswordEncoder createPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

工程还有一个 WebOptimizeSetting.java 类,它是目的是加速jsp的渲染,由于本工程的jsp页面非常简单,故而效果不明显。有无该类对工程无影响。

二、简易生产样例

本简易样例麻雀虽小,但五脏俱全,实现了以下特性:

  1. 自定义的登录界面
  2. 完整而严格的认证逻辑
    • 密码加密,数据库不存储密码明文
    • 用户账号增加是否启用校验
    • 用户账号增加锁定冻结校验
    • 用户账号增加过期校验
    • 账号的密码增加过期校验
  3. “记住我”功能
  4. 用户数据存储在数据库中
  5. URI 资源授权数据位于数据库中
  6. 在业务类中获取登录用户信息

2.1 项目结构

简易生产样例文件结构如下:

simple-industry-demo-code-structure

简易生产样例的代码量比快速demo多了不少,主要是因为 SpringSecurity 将可能涉及到的小功能点,均采用了单独接口的方式来组织,因此一个简单的特性所需要的功能点会被分散到多个类中。

2.2 用户数据存储到关系数据库(BS模式)

2.2.1 添加依赖

本示例使用的是 Sqlite 数据库,并采用了 MyBatis 作为ORM框架,因此需要新添加以下依赖

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.xerial</groupId>
        <artifactId>sqlite-jdbc</artifactId>
        <version>3.34.0</version>
    </dependency>
</dependencies>

2.2.2 MyBaitis 基本设置

在 application.yml 中添加 sqlite3 数据源和 mybatis 属性设置:

spring:
  # 指定Sqlite3数据源,需要注意日期格式参数
  datasource:
    driver-class-name: org.sqlite.JDBC
    url: jdbc:sqlite::resource:security-demo-db.sqlite3?date_string_format=yyyy-MM-dd HH:mm:ss

mybatis:
  configuration:
    map-underscore-to-camel-case: true

sqlite3 数据库位于 classpath 下,这为了方便演示。可以在这里下载 sqlite3 的桌面客户端工具,查看和操作该数据库。

2.2.3 创建用户、资源和权限

样例中涉及的实体为:用户、资源、权限。WEB项目而言,资源就是URL,而权限就只是一个普通的字符串,更确切地说,权限是一个名字,它通常对应我们现实生活中的某个职位的名称。比如:销售经理、纪委书记、副校长等。权限名称使用得最多的方式是角色名,这些权限拥有的具体权力是通过它拥有的资源来体现的。用户可以操作哪些资源,则是通过用户所拥有的权限(角色)来间接体现的。因此,还有一些中间表来记录权限与资源的关联系统、用户与权限的关联关系。下图是本示例中所有的数据库表间关系:

用户、资源、权限实体关系图

用户表中有2条记录,与快速演示样例中的账号相对应,内容如下:

id username password name mobile_phone is_enable is_locked account_expiry_time password_last_update_time
1 scallion codefate15218 葱葱 18866568848 1 0 2030-10-10 23:59:59 2026-09-09 23:59:59
2 lotus codefate15218 莲珍 15243436869 1 0 2030-10-10 23:59:59 2026-09-09 23:59:59

下面是这些数据库表在程序中的实体类

UserEntity
package guzb.cnblogs.security.industrydemo.auth;

import com.fasterxml.jackson.annotation.JsonIgnore;

import java.util.Date;

/** 
 * 自定义的用户明细数据库对象
 * 这里的源代码活力了Getter和Setter方法
 */
public class UserEntity {
    private Integer id;

    private String username;

    @JsonIgnore
    private String password;

    /** 姓名 */
    private String name;

    private String mobilePhone;

    private Boolean isEnabled;

    private Boolean isLocked;

    private Date accountExpiryTime;

    private Date passwordLastUpdateTime;

}

AuthorityEntity
package guzb.cnblogs.security.industrydemo.auth;

import org.springframework.security.core.GrantedAuthority;

/**
 * 权限数据库实体
 */
public class AuthorityEntity implements GrantedAuthority{

    /** 权限名称(英文),同时也是权限记录的主键 */
    private String name;

    /**  */
    private String title;

    /**
     * 权限类型
     * ROLE     : 角色
     * ORGANIZE : 组织
     **/
    private String type;

    @Override
    public String getAuthority() {
        // 这里最终会形成这样的权限文本串:ROLE_USER, ROLE_ADMIN 等
        return type + "_" + name;
    }

}

UriResourceEntity
package guzb.cnblogs.security.industrydemo.auth;

/**
 * 基于URI的权限资源,一个实例就代表一个资源
 */
public class UriResourceEntity {
    private String uri;

    private String httpMethod;
}
数据库访问对象SecurityMapper
package guzb.cnblogs.security.industrydemo.auth;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
 * Sqlite3数据的查询操作
 */
@Mapper
public interface SecurityMapper {

    @Select({
            "SELECT id, username, password, name, mobile_phone,",
            "is_enabled, is_locked, account_expiry_time, password_last_update_time",
            "FROM user WHERE username = #{username}"
    })
    UserEntity getByUsername(@Param("username") String username);

    @Select({
            "SELECT name, title, type",
            "FROM authority WHERE name IN",
            "(",
            "  SELECT authority_name",
            "  FROM user_authority_relation",
            "  WHERE user_id=#{userId}",
            ")"
    })
    List<AuthorityEntity> listAuthoritiesOfUser(@Param("userId") Integer userId);

    @Select({
            "select (a.type || '_' || a.name) as authority, a.title as resource_title, ",
            "h.uri as resource_uri, h.http_method as resource_http_method",
            "from http_resource_authority_relation r",
            "left join http_resource h on r.http_resource_id = h.id",
            "left join authority a on r.authority_name = a.name"
    })
    List<AuthorityResourceItem> listAllAuthorityResources();
}

2.2.4 提供UserDetailsService和PasswordEncoder

在「快速体验样例」中,使用的用户、权限和资源均基于内存配置,本示命中,这些数据都将从 sqlite3 数据库中加载,为此,我们需要提供一个UserService实现类,完成用户信息的加载,如下所示:

package guzb.cnblogs.security.industrydemo.auth;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

/**
 * 自定义的用户明细服务
 * 这里没有加@Service、@Component等类似注解,
 * 其装配过程写在了WebSecurityBeanConfig中,目的就是为了集中演示Security组件的装配
 */
public class MyUserDetailsService implements UserDetailsService {

    private SecurityMapper userEntityMapper;

    public MyUserDetailsService(SecurityMapper userEntityMapper) {
        this.userEntityMapper = userEntityMapper;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = userEntityMapper.getByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("用户 " + username + " 不存在");
        }

        List<AuthorityEntity> authorityEntityList = userEntityMapper.listAuthoritiesOfUser(userEntity.getId());
        return new MyUserDetails(userEntity, authorityEntityList);
    }
}

这里没有让这个 UserDetailsService 类被 Spring 容器自动装配,需要在 WebSecuritySetting 中手动装配,另外还需要提供一个 PasswordEncoder 来对密码进行加密,由于本示例中,sqlite3 的 user 表中,存储的是密码是明文,因此这个 PasswordEncoder 应该什么都不需要做,原样返回密码内容即可。如下所示(WebSecuritySetting 类的部分代码):

@Configuration
@EnableWebSecurity
public class WebSecuritySetting {
    @Autowired
    SecurityMapper userEntityMapper;

    /**
     * 提供一个默认的密码加密器,该加密器什么都不干,原样返回密码原文
     */
    @Bean
    public PasswordEncoder createPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 基于Sqlite3数据库的自定义用户明细服务
     */
    @Bean
    public MyUserDetailsService customerDbUserDetailService() {
        return new MyUserDetailsService(userEntityMapper);
    }
}

至此,BS模式下的重要组件就介绍完了,还有一个辅助类,没有在文中列出来,若列出来,篇幅就太长了。您可以在源码中查看它们。

现在可以将项目运行起来了,会发现,其页面交互效果与「快速体验样例」(见1.1演示效果)中是相同的,唯一不同的是,后端的用户、权限、资源信息已由程序硬编码,改为了从 sqlite3 数据库中加载了。

2.3 前后分离模式如何处理

相对于BS模式,前后端分离模式要麻烦一些。因为SpringSecurity诞生之初,前后端分离这种架构模式还在萌芽之中,因此整个 SpringSecurity 都仅考虑了BS模式下的处理方式。前后端分离主要的特点是:后端不需要返回用户界面,但需要返回相应的数据。具体来说,有以下事项需要处理

No. 场景 BS模式 前后端分离模式
1 认证成功 重定向到系统首页
或重定向到前一个访问的未授权页面
返回用户信息和凭证Token
2 认证失败 重定向到登录页面 返回失败原因
3 匿名用户访问未授权页面 重定向到登录页面 返回访问受限状态码和要求登录的提示文本
4 正常登录用户访问未授权页面 返回到无权限访问的提示页面 返回未授权错误状态码

从上表可以看出,相对于BS模式,前后端分离模式的所有差异都在 reponse 的处理上。 SpringSecurity 处理 response 的组件有:

  • AuthenticationSucessHanlder
    认证成功后,会调用此接口的 onAuthenticationSuccess 方法

  • AuthenticationFailureHandler
    认证失败后,会调用此接口的 onAuthenticationFailure 方法

  • ExceptionTranslationFilter
    当整个认证过程发生异常后,这个Filter会统一处理如何返回响应,根据异常类型的不同,处理方式可进一步细分到以下两个组件:

    • AccessDeniedHandler
      当访问一个未授权页面时,会抛出AccessDeniedException异常,此时 ExceptionTranslationFilter 会调用 AccessDeniedHandler 的 handle 方法生成 http response 内容,默认行为就是上表中的3和4。

    • AuthenticationEntryPoint
      当异常类型不是 AccessDeniedException 时,ExceptionTranslationFilter 会调用 AuthenticationEntryPoint 的 commence 方法生成 http response 内容。

一般情况下,我们都应该替换上面提到的四个接口实现类,通过返回 JSON 数据的方式来完成前后端分离的Web响应。但本示例没有完全这样做,而是新启用了一套认证 Filter 和 鉴权Filter。这样一来,BS模式和前后端分离模式在这个 Demo 中可以同时共存。但需要在 WebSecuritySetting 上装配这一套Filter, 如下所示(代码片段):

@Configuration
@EnableWebSecurity
public class WebSecuritySetting {
@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        // 1. 传统BS模式的认证配置(这一处省略,详情请下载源码查看)
        ......

        /*
         * 2. 前后端分离模式下的认证过滤器配置
         *    这里约定,uri 以 api 开头的,都属于前后端分离模式下的web资源,其中:
         *    /api/auth/** 是与认证相关的资源
         *    /api/** 下非 auth 的 uri, 是业务资源
         *
         * 2.1 前后端分离模式下的用户密码认证Provider和Filter
         **/
        AuthenticationManager parentAuthManager = httpSecurity.getSharedObject(AuthenticationManager.class);
        MyUsernamePasswordAuthenticationFilter myUsernamePasswordAuthenticationFilter = createMyUsernamePasswordAuthenticationFilter(parentAuthManager);
        httpSecurity.addFilterAfter(myUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // 2.2 前后端分离模式下的授权(访问控制)处理
        httpSecurity.authorizeHttpRequests(authorizeRegistry -> {
            authorizeRegistry
                    .antMatchers("/api/app/**")
                    .access(new MyUriAuthorizationManager(authorityService));
        });

        // 2.3 前后端分离模式下的访问拒绝逻辑处理, 必须添加在默认的 ExceptionTranslationFilter 过滤器之后
        MyUriAccessDeniedProcessingFilter restfulUriAccessDeniedFilter = new MyUriAccessDeniedProcessingFilter(objectMapper);
        httpSecurity.addFilterAfter(restfulUriAccessDeniedFilter, ExceptionTranslationFilter.class);

        // 2.4 放开前端的跨域限制,这一步仅仅是为了方便编写这个样例的前端页面。正常开发应该禁止跨域访问
        //     spring mvc的跨域配置是通过 覆盖 WebMvcConfigurer 的 addCorsMappings 方法或添加CorsFilter来实现的
        //     spring security 也为CorsFilter提供了添加入口,这里为了快速生效,使用了最简短的代码量来完成
        CorsConfigurationSource permitAllCorsConfig = request -> new CorsConfiguration().applyPermitDefaultValues();
        httpSecurity.cors().configurationSource(permitAllCorsConfig);

        return httpSecurity.build();
    }
}

下面是前后端分离模式的演示截图:

前后端分离模式页面演示动图

这个演示页面的源码位于 webapp/WEB-INF/views/rest.jsp 中,它是一个采用 vue + axios 模拟的单页应用

尽管代码量不大,但涉及到的类数量有些多(见2.1小节),不便在页面上一一展示,你可以从这里下载工程源码并运行, 查看完整的交互效果。

2.4 Session 问题

如果您下载了这个小节的工程源码,并运行体验了一下,便会发现,BS模式和前后端端分离模式在这个 Demo 中是同时有效的。即通过BS页面登录后,再访问前后页面页面中的接口,就可以获取到正确数据。返过来也一样,在前后端分离的单应用页面上登录了,再回退到BS页面,访问资源页面时,也能够得到资源响应页面。其实二者在认证时,采用了不同的 Authentication 实现类,但他们的 Session 是共享的,因为默认情况下,Spring Security 的 session 机制是基于 http cookie 的, BS模式认证后的 session 与前后端分离模式认证后的 session, 都是基于 JSESSIONID 这个 http cookie 来追踪的。

当然,真实情况下,前后分离模式的 Session 机制需要单独编码处理,一般是将生成的 token 和用户基于信息存储到redis和本地缓存中,通过token来追踪。也可以采用 JWT 这种 Token 来实现完全的无状态会话,服务器端不再追踪 Session 状态。

SpringSecurity 默认提供了 SessionAuthenticationStrategy 接口来完成 session 的创建。正统的方法是:实现一个自己的 SessionAuthenticationStrategy 类,替换掉框架实现。但更简易的做法是:重写一个 Filter,将自己的处理逻辑置于其中,只是这样一来,本质上已与 Spring Security 关系不大了。

三、小结

本文试图通过「快速演示样例」和「简易工业样例」两个小 Demo 来向您展示如何快速编写一个基于 Spring Security 的 Web 工程,以便可以快速体验到 Seping Security 的特点。简易工程样例由于代码较多,已无法在文章中全部呈现。

那么问题来了,既然代码量比较大,那它为什么还敢叫「简易」工业样例呢?哈哈 😄 。 可别忘了,本专辑属于《从入门到放弃》系列,后面还将更详细的介绍 Spring Security 的完整处理流程。细节多到直接将人劝退,一个简单的认证功能,为适应框架的设计特点,会将原本集中的认证逻辑打散得漫山遍野。这就是灵活性的代价(副作用)。介时再回过头来看这个「简易工业样例」你就会说,嗯,这个样例确实很简易。

尽管如此,Spring Security 的确是做Web认证和鉴权方面的优秀框架。它的超灵活性(扩展性)给使用者带来了十分陡峭的学习曲线,这确实阻碍了它在普通项目开发中的应用推广,毕竟我们日常开发中涉及到的认证和鉴权场景都比较简单清晰的,尤其是在国内,一些特殊的鉴权需求,以 Spring Security 的方式强行实现的话,会水土不服,概念上很别扭,不利于后期维护。但这个框架是值得您学习的,一来它确实非常优秀,另外一方面,国内也有不少项目的认证和鉴权是采用Spring Security 来实现的,接触这些项目的可能性还是很高的。

四、工程源码

posted @ 2023-08-08 09:07  顾志兵  阅读(514)  评论(1编辑  收藏  举报