65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MySQL 和 Spring Boot 进行会话管理

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2021年4月29日

MIT

12分钟阅读

viewsIcon

15455

downloadIcon

190

本教程将讨论一种高级配置,即使用 MySQL 数据库进行会话管理。

引言

一个使用表单认证的安全 Web 应用程序需要会话。对于我自己的项目,我一直使用 Spring Boot 的默认会话。它要么将会话存储在内存中,要么存储在某些临时文件中。这是一个非常有限的解决方案。而且它只在我将应用程序托管在单个服务器上时效果很好。如果我需要使其更具可伸缩性,例如我需要三个 Web 服务器共享相同 的会话数据,那么我需要使用比默认配置更好的东西。

根据我在 ASP.NET 方面的经验,我知道有一种方法可以将会话数据存储在数据库中。使用 Spring Boot、Spring MVC,我想我可以做类似的事情。Spring 框架与 ASP.NET 非常相似,前提是你能看穿它们之间的差异。困难的部分是弄清楚配置是如何工作的。

我使用关键词“Spring Session MySQL”在网上搜索,列表显示同一个教程出现在多个搜索结果页面。我本来打算在这个教程中复制相同的方法。然而,经过对配置的仔细检查,我决定不这样做。您有福了。本教程将提供不同的解决方案。我认为这比在所有 Google 搜索结果页面中复制的方法更实用。您可以自行决定。

应用程序架构

对于本教程,我打算创建一个简单的示例应用程序来阐明观点。对我来说,会话数据应该与应用程序特定的数据存储在不同的数据库中。您可以将两者混合在同一个数据库中,但它们服务于两个不同的目的,并将它们放在同一个数据库中似乎是错误的。我们不知道会话中的数据会有多少进出,也不知道应用程序的数据会有多少进出。我们也不知道这两组数据需要何种安全级别或保护。还有其他关于这两组数据的考虑因素。重点是,最好将它们分成两个不同的数据库。

当我搜索如何使用 MySQL 数据库进行会话存储时,出现的教程使用了单个数据源。所有配置都在 `application.properties` 配置文件中完成。我进行了更深入的研究,但找不到更好的方法来拥有两个数据源。更糟糕的是,多个教程使用了完全相同的代码集。我再研究了一下,幸运地找到了一个解决方案。我在这里分享它,以便每个人都能从中受益。

如前所述,示例应用程序有一个登录功能,用户成功登录后,索引页面将加载应用程序数据库中的所有行。用户成功登录后,会创建会话,并可以在会话数据库中查询。我重用了我 上一个教程(UTF-8 编码的 Web 应用程序)中的相同数据库设计。要显示的数据将是 UUID 作为行 ID,标题(一个短字符串),以及内容(一个长字符串)。这里的想法是,一旦用户能够登录,会话就成功创建。会话数据将包含用户何时登录以及何时强制退出的信息。在用户登录时,数据可以从另一个数据库加载,并在索引页面上显示。这非常简单,关键是我想演示如何进行配置以使其正常工作。

数据库设计

让我从数据库设计开始。对于这个示例应用程序,有两个数据库,一个是会话数据的,一个是应用程序特定数据的。应用程序数据的数据库设计非常简单,与 上一个教程完全相同。更重要的是会话数据库。在应用程序执行之前创建数据库至关重要。我们将在应用程序配置代码中解释原因。

让我们来看看会话数据库的设计。为了创建这个数据库,我必须

  1. 创建数据库架构
  2. 创建数据库表

第一步是我们有一些自由。我可以定义数据库架构名称、可以访问它的用户以及用户的密码。因为数据以字母数字字符的形式存储,所以我不需要使用 UTF-8 编码。这是我定义此数据库的 SQL 脚本

DROP DATABASE IF EXISTS `sessiondb`;
DROP USER IF EXISTS 'sndbuser'@'localhost';
CREATE DATABASE `sessiondb`;
CREATE USER 'sndbuser'@'localhost' IDENTIFIED BY '123$Test$321';
GRANT ALL PRIVILEGES ON `sessiondb`.* TO 'sndbuser'@'localhost';
FLUSH PRIVILEGES;

这是一个简单的脚本,前两行用于删除数据库架构和与此数据库关联的用户。然后接下来的两行将创建数据库架构和用户。第五行授予用户该数据库的所有权限。最后一行将刷新更改,使其立即生效。

接下来,我需要创建实际存储会话数据的表。表必须具有特定的名称和特定的列。这是 Spring Framework 提供的。您可以从特定的 jar 文件中获取脚本。对于这个示例应用程序,我使用的是 2.3.1 版本的 Spring Boot 和相关的 Spring 依赖项。因此,我需要提取的脚本来自 `spring-session-jdbc-2.3.1.RELEASE.jar`。如果您需要不同版本,只需查找 `spring-session-jdbc-<您目标版本>.RELEASE.jar`。您可以使用存档管理器浏览此 jar 的内部结构,您会找到很多数据库脚本,它们的名称是“`schema-<database type>.sql`”。我需要的是“`schema-mysql.sql`”。我只需提取该文件并针对我的 MySQL 数据库架构执行它即可创建表。下面是它的样子

CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BLOB NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) _
               REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;   

为了创建这些表,我必须在上述脚本的顶部添加以下内容

use `sessiondb`;

这是我的应用程序数据的数据库架构。如前所述,我重用了我 上一个教程中的相同数据库

DROP DATABASE IF EXISTS `utf8testdb`;
DROP USER IF EXISTS 'utf8tdbuser'@'localhost';
CREATE DATABASE `utf8testdb`;
CREATE USER 'utf8tdbuser'@'localhost' IDENTIFIED BY '123$Test$321';
GRANT ALL PRIVILEGES ON `utf8testdb`.* TO 'utf8tdbuser'@'localhost';
FLUSH PRIVILEGES;
ALTER DATABASE `utf8testdb` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

这是我的应用程序的数据表

USE `utf8testdb`;

DROP TABLE IF EXISTS `testcontent`;

CREATE TABLE `testcontent` (
   `id` VARCHAR(34) NOT NULL PRIMARY KEY,
   `subject` VARCHAR(512) NOT NULL,
   `content` MEDIUMTEXT NOT NULL
);

下一部分将是应用程序的配置代码。我将解释为什么我必须先创建会话数据库。

应用程序配置

在本节中,我将向您展示应用程序是如何配置以启动的,以便它可以正确使用会话和应用程序数据库。让我们从应用程序的主入口开始

package org.hanbo.boot.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App
{
   public static void main(String[] args)
   {
      SpringApplication.run(App.class, args);
   }
}

这是 Spring Boot 应用程序的标准主入口。接下来,我需要为表单认证设置安全配置。我从我早期关于 Spring Security 的 ThymeLeaf 教程中获取了它。这是该配置的样子

package org.hanbo.boot.app.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.
                           builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.
                           EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.
                           WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.
                           SavedRequestAwareAuthenticationSuccessHandler;
import org.hanbo.boot.app.security.UserAuthenticationService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebAppSecurityConfig extends WebSecurityConfigurerAdapter
{
   @Autowired
   private UserAuthenticationService authenticationProvider;
   
   @Autowired
   private AccessDeniedHandler accessDeniedHandler;
   
   @Override
   protected void configure(HttpSecurity http) throws Exception
   {
      http
      .authorizeRequests()
          .antMatchers("/assets/**", "/public/**").permitAll()
          .anyRequest().authenticated()
      .and()
      .formLogin()
          .loginPage("/login")
          .permitAll()
          .usernameParameter("username")
          .passwordParameter("userpass")
          .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
          .defaultSuccessUrl("/secure/index", true).failureUrl("/public/authFailed")
          .and()
      .logout().logoutSuccessUrl("/public/logout")
          .permitAll()
          .and()
      .exceptionHandling().accessDeniedHandler(accessDeniedHandler);
   }
   
   @Override
   protected void configure(AuthenticationManagerBuilder authMgrBuilder)
      throws Exception
   {
      authMgrBuilder.authenticationProvider(authenticationProvider);
   }
}

我可以通过多种方式演示会话生命周期,但使用 Spring Security 进行演示是最实用的。如果您能够成功运行此应用程序并使用测试用户登录,则意味着会话配置正确。您甚至可以查询会话数据库以查看其中存储的数据。

这是我设置测试用户以访问安全索引页面的方法

package org.hanbo.boot.app.security;

import java.util.ArrayList;
import java.util.List;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

@Service
public class UserAuthenticationService
implements AuthenticationProvider
{
   @Override
   public Authentication authenticate(Authentication auth) throws AuthenticationException
   {
      Authentication retVal = null;
      List<GrantedAuthority> grantedAuths = new ArrayList<GrantedAuthority>();
      
      if (auth != null)
      {
         String name = auth.getName();
         String password = auth.getCredentials().toString();
         System.out.println("name: " + name);
         System.out.println("password: " + password);
         
         if (name.equals("user1") && password.equals("user12345"))
         {
            grantedAuths.add(new SimpleGrantedAuthority("ROLE_USER"));
            
            retVal = new UsernamePasswordAuthenticationToken(
               name, "", grantedAuths
            );
            System.out.println("grant User");
         }
      }
      else
      {
         System.out.println("invalid login");
         retVal = new UsernamePasswordAuthenticationToken(
            null, null, grantedAuths
         );
         System.out.println("bad Login");
      }

      return retVal;
   }

   @Override
   public boolean supports(Class<?> tokenType)
   {
      return tokenType.equals(UsernamePasswordAuthenticationToken.class);
   }
}

到目前为止,我还没有展示会话设置或数据库连接配置。数据源的配置是最重要的部分。它们可以放在一起,方法如下

package org.hanbo.boot.app.config;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.session.jdbc.config.annotation.SpringSessionDataSource;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;

@EnableJdbcHttpSession
@Configuration
public class DataAccessConfiguration
{
   @Value("${db.jdbc.driver}")
   private String dbJdbcDriver;
   
   @Value("${db.conn.string}")
   private String dbConnString;
   
   @Value("${db.access.username}")
   private String dbAccessUserName;

   @Value("${db.access.password}")
   private String dbAccessPassword;

   @Value("${db.access.validity.query}")
   private String dbAccessValityQuery;   
   
   @Value("${db.session.conn.string}")
   private String dbSesnConnString;
   
   @Value("${db.session.access.username}")
   private String dbSesnAccessUserName;

   @Value("${db.session.access.password}")
   private String dbSesnAccessPassword;

   @Value("${db.session.access.validity.query}")
   private String dbSesnAccessValityQuery;
   
   @Bean
   public DataSource contentDataSource()
   {
      BasicDataSource dataSource = new BasicDataSource();

      dataSource.setDriverClassName(dbJdbcDriver);
      dataSource.setUrl(dbConnString);
      dataSource.setUsername(dbAccessUserName);
      dataSource.setPassword(dbAccessPassword);
      dataSource.setMaxIdle(4);
      dataSource.setMaxTotal(20);
      dataSource.setInitialSize(4);
      dataSource.setMaxWaitMillis(900000);
      dataSource.setTestOnBorrow(true);
      dataSource.setValidationQuery(dbAccessValityQuery);
      
      return dataSource;
   }
   
   @SpringSessionDataSource
   @Bean
   public DataSource sessionDataSource()
   {
      BasicDataSource dataSource = new BasicDataSource();

      dataSource.setDriverClassName(dbJdbcDriver);
      dataSource.setUrl(dbSesnConnString);
      dataSource.setUsername(dbSesnAccessUserName);
      dataSource.setPassword(dbSesnAccessPassword);
      dataSource.setMaxIdle(4);
      dataSource.setMaxTotal(20);
      dataSource.setInitialSize(4);
      dataSource.setMaxWaitMillis(900000);
      dataSource.setTestOnBorrow(true);
      dataSource.setValidationQuery(dbSesnAccessValityQuery);
      
      return dataSource;
   }
   
   @Bean
   public NamedParameterJdbcTemplate namedParameterJdbcTemplate()
   {
      NamedParameterJdbcTemplate retVal
          = new NamedParameterJdbcTemplate(contentDataSource());
       return retVal;
   }

   @Bean
   public DataSourceTransactionManager txnManager()
   {
      DataSourceTransactionManager txnManager
         = new DataSourceTransactionManager(contentDataSource());
      return txnManager;
   }
}

对于这个类,我使用了两个注解,一个是 `&#064;EnableJdbcHttpSession`。另一个是 `&#064;Configuration`。当这个类被加载时,它将被注册到 Spring IOC 容器中,作为提供配置的类,它还将使应用程序能够使用数据库和 JDBC 来管理应用程序会话

...
@EnableJdbcHttpSession
@Configuration
public class DataAccessConfiguration
{
...
}

此应用程序需要两个数据库,我必须以某种方式提供两个不同的数据源(我稍后会展示如何定义数据源)。这就是为什么我必须使用 `&#064;EnableJdbcHttpSession` 注解,而不是应用程序属性文件来配置数据库的使用以进行会话管理。这也是为什么我必须手动设置会话数据库,而不是使用应用程序属性文件来处理数据库创建。我告诉过您我将解释为什么必须提前创建数据库。

如您所见,有很多私有字符串属性。这些属性是从应用程序属性文件中注入的。它们代表两个不同的数据库连接

   // application database connection properties
   @Value("${db.jdbc.driver}")
   private String dbJdbcDriver;
   
   @Value("${db.conn.string}")
   private String dbConnString;
   
   @Value("${db.access.username}")
   private String dbAccessUserName;

   @Value("${db.access.password}")
   private String dbAccessPassword;

   @Value("${db.access.validity.query}")
   private String dbAccessValityQuery;   
   
   // session database connection properties
   @Value("${db.session.conn.string}")
   private String dbSesnConnString;
   
   @Value("${db.session.access.username}")
   private String dbSesnAccessUserName;

   @Value("${db.session.access.password}")
   private String dbSesnAccessPassword;

   @Value("${db.session.access.validity.query}")
   private String dbSesnAccessValityQuery;

这是 `application.properties` 文件看起来的样子,您可以看到这些值是如何注入到这些 `private` 属性中的

db.jdbc.driver=com.mysql.cj.jdbc.Driver
db.conn.string=jdbc:mysql://:3306/utf8testdb?useUnicode=true&
useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&
serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false
db.access.username=utf8tdbuser
db.access.password=123$Test$321
db.access.validity.query=SELECT 1

db.session.conn.string=jdbc:mysql://:3306/sessiondb?useUnicode=true&
useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&
allowPublicKeyRetrieval=true&useSSL=false
db.session.access.username=sndbuser
db.session.access.password=123$Test$321
db.session.access.validity.query=SELECT 1

接下来,我想定义数据源。数据源对象包含连接信息,可用于创建到数据库的 SQL 连接。这是我定义应用程序数据库数据源的方法

   @Bean
   public DataSource contentDataSource()
   {
      BasicDataSource dataSource = new BasicDataSource();

      dataSource.setDriverClassName(dbJdbcDriver);
      dataSource.setUrl(dbConnString);
      dataSource.setUsername(dbAccessUserName);
      dataSource.setPassword(dbAccessPassword);
      dataSource.setMaxIdle(4);
      dataSource.setMaxTotal(20);
      dataSource.setInitialSize(4);
      dataSource.setMaxWaitMillis(900000);
      dataSource.setTestOnBorrow(true);
      dataSource.setValidationQuery(dbAccessValityQuery);
      
      return dataSource;
   }

这里是应用程序中最重要的部分,定义另一个特殊的数据源,用于会话数据库

   @SpringSessionDataSource
   @Bean
   public DataSource sessionDataSource()
   {
      BasicDataSource dataSource = new BasicDataSource();

      dataSource.setDriverClassName(dbJdbcDriver);
      dataSource.setUrl(dbSesnConnString);
      dataSource.setUsername(dbSesnAccessUserName);
      dataSource.setPassword(dbSesnAccessPassword);
      dataSource.setMaxIdle(4);
      dataSource.setMaxTotal(20);
      dataSource.setInitialSize(4);
      dataSource.setMaxWaitMillis(900000);
      dataSource.setTestOnBorrow(true);
      dataSource.setValidationQuery(dbSesnAccessValityQuery);
      
      return dataSource;
   }

您现在应该明白了。`&#064;SpringSessionDataSource` 注解表示它仅用于会话管理以连接到数据库。这就是我定义两个数据源的方式。我在 Spring Boot 官方文档网站上找到了它。这是一个幸运的发现。然后,我又可以找到我需要的一切。一旦我弄清楚了这一点,所有技术问题就都解决了。最后两个方法是定义一个 JDBC 模板对象和一个事务管理器对象

   @Bean
   public NamedParameterJdbcTemplate namedParameterJdbcTemplate()
   {
      NamedParameterJdbcTemplate retVal
          = new NamedParameterJdbcTemplate(contentDataSource());
       return retVal;
   }

   @Bean
   public DataSourceTransactionManager txnManager()
   {
      DataSourceTransactionManager txnManager
         = new DataSourceTransactionManager(contentDataSource());
      return txnManager;
   }

到目前为止,会话配置已完成。我需要一个控制器和一个请求处理器来演示会话与 Spring Security 的结合使用

package org.hanbo.boot.app.controllers;

import java.util.ArrayList;
import java.util.List;

import org.hanbo.boot.app.models.PostDataModel;
import org.hanbo.boot.app.services.PostDataService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class SecuredPageController
{
   private PostDataService _dataSvc;
   
   public SecuredPageController(
      PostDataService svc)
   {
      _dataSvc = svc;
   }
   
   @PreAuthorize("hasRole('USER')")
   @RequestMapping(value="/secure/index", method = RequestMethod.GET)
   public ModelAndView index1()
   {
      List<PostDataModel> allPosts = _dataSvc.getAllPostData();
      if (allPosts == null)
      {
         allPosts = new ArrayList<PostDataModel>();
      }
      System.out.println(allPosts.size());
      
      ModelAndView retVal = new ModelAndView();
      retVal.setViewName("indexPage");
      retVal.addObject("allPosts", allPosts);
      
      return retVal;
   }
}

这是一个非常简单的 MVC 控制器。这个类中唯一的 `method` 处理将显示安全索引页面的请求。它将使用一个注入的服务对象,该对象从应用程序数据库加载数据,并将加载的数据列表返回给索引页面。请注意,该 `method` 被标记了 Spring Security 注解 `&#064;PreAuthorize("hasRole('USER')")`。这意味着任何具有“`USER”角色的用户都可以访问此页面。

为了使上述 MVC 控制器正常工作,我定义了一个 `model` 类和一个 `service` 类。我不会展示 `model` 类,因为它非常简单。这是 `service` 的实现类

package org.hanbo.boot.app.services;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import org.hanbo.boot.app.models.PostDataModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class PostDataServiceImpl implements PostDataService
{
   private final String sql_insertPostData = 
   "INSERT INTO `testcontent` (id, subject, content) VALUES (:id, :title, :content);";

   private final String sql_queryAllPosts = 
           "SELECT id, subject, content FROM `testcontent` LIMIT 1000;";
   
   @Autowired
   private NamedParameterJdbcTemplate sqlDao;
   
   @Override
   @Transactional
   public String savePostData(PostDataModel dataToSave)
   {
      if (dataToSave != null)
      {
         String title = dataToSave.getTitle();
         if (title == null || title.isEmpty())
         {
            throw new RuntimeException("Title is NULL or empty");
         }
         
         String content = dataToSave.getContent();
         if (content == null || content.isEmpty())
         {
            throw new RuntimeException("Content is NULL or empty");
         }
         
         Map<String, Object> parameters = new HashMap<String, Object>();
         
         String postId = generateId();
         parameters.put("id", postId);
         parameters.put("title", dataToSave.getTitle());
         parameters.put("content", dataToSave.getContent());
         
         int updateCount = sqlDao.update(sql_insertPostData, parameters);
         if (updateCount > 0)
         {
            return postId;
         }         
      }
      
      return "";
   }

   @Override
   public List<PostDataModel> getAllPostData()
   {
      List<PostDataModel> retVal = new ArrayList<PostDataModel>();
      
      retVal = sqlDao.query(sql_queryAllPosts,
            (MapSqlParameterSource)null,
            (rs) -> {
               List<PostDataModel> foundObjs = new ArrayList<PostDataModel>();
               if (rs != null)
               {
                  while (rs.next())
                  {
                     PostDataModel postToAdd = new PostDataModel();
                     postToAdd.setPostId(rs.getString("id"));
                     postToAdd.setTitle(rs.getString("subject"));
                     postToAdd.setContent(rs.getString("content"));
                     
                     foundObjs.add(postToAdd);
                  }
               }
               
               return foundObjs;
            });

      return retVal;
   }

   private static String generateId()
   {
      UUID uuid = UUID.randomUUID();
      String retVal = uuid.toString().replaceAll("-", "");
      
      return retVal;
   }
}

我展示这个的原因是它使用了 JDBC 模板对象进行数据库访问。这个 JDBC 模板对象可以访问的是应用程序数据库。当用户登录并可以使用此服务时,会话数据库将包含代表此会话的行。

最后,我想展示 Maven POM 文件,然后是此示例应用程序的测试。

Maven POM 文件

在 Maven POM 文件中,我唯一想指出的是依赖项。我添加了两个新的依赖项,一个是 `spring-session-jdbc`,另一个是 `spring-boot-starter-data-jpa`。`spring-session-jdbc` 是使用数据库进行会话管理所需的依赖项。我认为我不需要另一个依赖项,您可以尝试删除它,然后运行应用程序并查看它是否有效。这是 POM 文件依赖项的整个部分

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.session</groupId>
         <artifactId>spring-session-jdbc</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>8.0.11</version>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-tomcat</artifactId>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <dependency>
         <groupId>org.thymeleaf.extras</groupId>
         <artifactId>thymeleaf-extras-springsecurity5</artifactId>
         <version>3.0.4.RELEASE</version>
      </dependency>
      <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-dbcp2</artifactId>
         <version>2.5.0</version>
      </dependency>
   </dependencies>

如何测试示例应用程序

在获取 zip 文件格式的源文件后,请将所有 `*.sj` 文件重命名为 `*.js` 文件。

然后,在项目根目录下(POM 文件所在的位置),运行以下命令

mvn clean install

成功构建项目后,您可以使用以下命令运行应用程序

java -jar target/hanbo-spring-session-mysql-sample-1.0.0.jar

假设应用程序成功运行,您可以通过访问以下 URL 来访问登录页面

https://:8080

只有一个用户可供登录,用户名为“`user1”,密码为“`user12345”。如果您可以使用此用户登录并看到索引页面显示,那么您就知道会话正在工作。如果应用程序配置有任何问题,您很可能会看到显示错误代码 500(内部服务器错误)的错误页面。

如果您成功登录并执行了应用程序数据库的插入脚本,您将看到

安全索引页面将显示应用程序数据库中的帖子列表,因此请使用脚本“`inserts.sql”(位于项目根文件夹下的 `DB` 子文件夹中)将这些数据行添加到应用程序数据库的唯一表中。

一旦您成功登录到应用程序,您就可以检查会话数据库并查看您的会话数据

Click to enlarge image

Spring 会话管理有两个数据表。上一个是显示来自表“`SPRING_SESSION”的查询结果。下面是来自第二个表“`SPRING_SESSION_ATTRIBUTES;”的查询结果

Click to enlarge image

对于这个示例应用程序,我提供了注销功能。当用户注销时,查询会话数据表,您将看到

Click to enlarge image

还有这个

Click to enlarge image

没错!一旦您注销,会话表中将清除与该会话相关的数据。如果数据库有多个会话,那么被清除的将只是用户注销的那一个。

摘要

这是一个相当复杂的教程。示例应用程序涉及很多移动部件。我必须让它正常工作的是以下几点

  1. 创建会话数据库和表。
  2. 创建应用程序数据库。
  3. 插入任何需要的应用程序数据。会话数据库最初应为空。
  4. 添加 Spring Security 和 Spring JDBC 的配置(特别是为会话访问提供第二个数据源)。
  5. 添加 MVC 控制器、服务类和数据访问类。在 MVC 控制器上,使用 `&#064;PreAuthorize` 注解来强制用户登录。

关键在于我可以先手动创建会话数据库,然后使用两个数据源,其中一个专用于会话访问。所有这一切只需要一个 `&#064;SpringSessionDataSource` 注解。其余的都一样。这是另一个看起来棘手但总有简单解决方案的技术问题。

只想提一下最后一件事,我研究了使用 Google OpenID 进行身份验证,并决定不使用它。设置并不难,但是由于我必须使用基于 Google 的 API 并且必须暴露我自己的 Google 访问的许多配置,因此安全风险太高了。而且没有功能需求来使用 Google OpenID。所以今年我不会提供关于如何使用 Spring Security 配置 OpenID 的教程。

希望您喜欢本教程。我创作它时很开心。更多教程即将推出。敬请期待!

历史

  • 2021 年 4 月 27 日 - 初稿
© . All rights reserved.