使用 MySQL 和 Spring Boot 进行会话管理
本教程将讨论一种高级配置,即使用 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,标题(一个短字符串),以及内容(一个长字符串)。这里的想法是,一旦用户能够登录,会话就成功创建。会话数据将包含用户何时登录以及何时强制退出的信息。在用户登录时,数据可以从另一个数据库加载,并在索引页面上显示。这非常简单,关键是我想演示如何进行配置以使其正常工作。
数据库设计
让我从数据库设计开始。对于这个示例应用程序,有两个数据库,一个是会话数据的,一个是应用程序特定数据的。应用程序数据的数据库设计非常简单,与 上一个教程完全相同。更重要的是会话数据库。在应用程序执行之前创建数据库至关重要。我们将在应用程序配置代码中解释原因。
让我们来看看会话数据库的设计。为了创建这个数据库,我必须
- 创建数据库架构
- 创建数据库表
第一步是我们有一些自由。我可以定义数据库架构名称、可以访问它的用户以及用户的密码。因为数据以字母数字字符的形式存储,所以我不需要使用 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;
}
}
对于这个类,我使用了两个注解,一个是 `@EnableJdbcHttpSession`。另一个是 `@Configuration`。当这个类被加载时,它将被注册到 Spring IOC 容器中,作为提供配置的类,它还将使应用程序能够使用数据库和 JDBC 来管理应用程序会话
...
@EnableJdbcHttpSession
@Configuration
public class DataAccessConfiguration
{
...
}
此应用程序需要两个数据库,我必须以某种方式提供两个不同的数据源(我稍后会展示如何定义数据源)。这就是为什么我必须使用 `@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;
}
您现在应该明白了。`@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 注解 `@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` 子文件夹中)将这些数据行添加到应用程序数据库的唯一表中。
一旦您成功登录到应用程序,您就可以检查会话数据库并查看您的会话数据
Spring 会话管理有两个数据表。上一个是显示来自表“`SPRING_SESSION”的查询结果。下面是来自第二个表“`SPRING_SESSION_ATTRIBUTES;”的查询结果
对于这个示例应用程序,我提供了注销功能。当用户注销时,查询会话数据表,您将看到
还有这个
没错!一旦您注销,会话表中将清除与该会话相关的数据。如果数据库有多个会话,那么被清除的将只是用户注销的那一个。
摘要
这是一个相当复杂的教程。示例应用程序涉及很多移动部件。我必须让它正常工作的是以下几点
- 创建会话数据库和表。
- 创建应用程序数据库。
- 插入任何需要的应用程序数据。会话数据库最初应为空。
- 添加 Spring Security 和 Spring JDBC 的配置(特别是为会话访问提供第二个数据源)。
- 添加 MVC 控制器、服务类和数据访问类。在 MVC 控制器上,使用 `@PreAuthorize` 注解来强制用户登录。
关键在于我可以先手动创建会话数据库,然后使用两个数据源,其中一个专用于会话访问。所有这一切只需要一个 `@SpringSessionDataSource` 注解。其余的都一样。这是另一个看起来棘手但总有简单解决方案的技术问题。
只想提一下最后一件事,我研究了使用 Google OpenID 进行身份验证,并决定不使用它。设置并不难,但是由于我必须使用基于 Google 的 API 并且必须暴露我自己的 Google 访问的许多配置,因此安全风险太高了。而且没有功能需求来使用 Google OpenID。所以今年我不会提供关于如何使用 Spring Security 配置 OpenID 的教程。
希望您喜欢本教程。我创作它时很开心。更多教程即将推出。敬请期待!
历史
- 2021 年 4 月 27 日 - 初稿