实战开发,使用 Spring Session 与 Spring security 完成网站登录改造!!
上次小黑在文章中介绍了四种分布式一致性 Session 的实现方式,在这四种中最常用的就是后端集中存储方案,这样即使 web 应用重启或者扩容,Session 都没有丢失的风险。

今天我们就使用这种方式对 Session 存储方式进行改造,将其统一存储到 Redis 中。
实现方案
我们先来想一下,如果我们不依靠任何框架,自己如何实现后端 Session 集中存储。
这里我们假设我们的网站除了某些页面,比如首页可以直接访问以外,其他任何页面都需要登录之后才能访问。
如果需要实现这个需求,这就需要我们对每个请求都进行鉴权,鉴权目的是为了判断用户是否登录,判断用户角色。
如果用户没有登录,我们需要将请求强制跳转到登录页面进行登录。
用户登录之后,我们需要将登录获取到的用户信息存储到 Session 中,这样后面请求鉴权只需要判断 Session 中是否存在即可。
知道整个流程之后,其实实现原理就不是很难了。
我们可以使用类似 AOP 的原理,在每个请求进来之后,都先判断 Session 中是否存在用户信息,如果不存在就跳转到登录页。
整个流程如下所示:

我们可以利用 Servelt Filter 实现上述流程,不过上述整套流程,Spring 已经帮我们实现了,那我们就不用重复造轮子了。
我们可以使用 Spring-Session 与 Spring-security 实现上述网站的流程。
Spring-Session 是 Spring 提供一套管理用户 Session 的实现方案,使用 Spring-Session 之后,默认 WEB 容器,比如 Tomcat,产生的 Session 将会被 Spring-Session 接管。
除此之外,Spring-Session 还提供几种常见后端存储实现方案,比如 Redis,数据库等。
有了 Spring-Session 之后,它只是帮我们解决了 Session 后端集中存储。但是上述流程中我们还需要登录授权,而这一块我们可以使用 Spring-security 来实现。
Spring-security 可以维护统一的登录授权方式,同时它可以结合 Spring-Session 一起使用。用户登录授权之后,获取的用户信息可以自动存储到 Spring-Session 中。
好了,不说废话了,我们来看下实现代码。
下述使用 Spring Boot 实现, Spring-Boot 版本为:2.3.2.RELEASE
Spring Session
首先我们引入 Spring Session 依赖,这里我们使用 Redis 集中存储 Session 信息,所以我们需要下述依赖即可。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
如果不是 Spring Boot 项目,那主要需要引入如下依赖:
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.3.0.RELEASE</version>
</dependency>
引入依赖之后,我们首先需要在 application.properties增加 Session 相关的配置:
## Session 存储方式
spring.session.store-type=redis
## Session 过期时间,默认单位为 s
server.servlet.session.timeout=600
## Session 存储到 Redis 键的前缀
spring.session.redis.namespace=test:spring:session
## Redis 相关配置
spring.redis.host=127.0.0.1
spring.redis.password=****
spring.redis.port=6379
配置完成之后,Spring Session 就会开始管理 Session 信息,下面我们来测试一下:
@ResponseBody
@GetMapping("/hello")
public String hello() {
return "Hello World";
}
当我们访问上面地址之后,访问 Redis ,可以看到存储的 Session 信息。
推荐大家一个 Redis 客户端「Another Redis DeskTop Manager」,这个客户端 UI 页面非常漂亮,操作也很方便,下载地址:
https://github.com/qishibo/anotherredisdesktopmanager/releases

默认情况下,Session 默认使用HttpSession 序列化方式,这种值看起来不够直观。我们可以将其修改成 json 序列化方式,存储到 redis 中。
@Configuration
public class HttpSessionConfig implements BeanClassLoaderAware {
private ClassLoader loader;
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer(objectMapper());
}
/**
* Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
* constructors
*
* @return the {@link ObjectMapper} to use
*/
private ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
return mapper;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.loader = classLoader;
}
}
修改之后 Redis 键值如下所示:
ps:这里 Redis 键值含义,下次分析源码的时候,再做分析。
Spring Session 还存在一个 @EnableRedisHttpSession,我们可以在这个注解上配置 Spring Session 相关配置。
@EnableRedisHttpSession(redisNamespace = "test:session")
需要注意的是,如果使用这个注解,将会导致 application.properties Session 相关配置失效,也就是说 Spring Session 将会直接使用注解上的配置。

这里小黑比较推荐大家使用配置文件的方式。
好了,Spring Session 到这里我们就接入完成了。
Spring security
上面我们集成了 Spring Session,完成 Session 统一 Redis 存储。接下来主要需要实现请求的登陆鉴权。
这一步我们使用 Spring security 实现统一的登陆鉴权服务,同样的框架的还有 Shiro,这里我们就使用 Spring 全家桶。
首先我们需要依赖的相应的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入上面的依赖之后,应用启动之后将会生成一个随机密码,然后所有的请求将会跳转到一个 Spring security 的页面。


这里我们需要实现自己业务的登陆页,所以我们需要自定义登录校验逻辑。
在 Spring security 我们只需要实现 UserDetailsService接口,重写 loadUserByUsername方法逻辑。
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 简单起见,直接内部校验
String uname = "admin";
String passwd = "1234qwer";
// 如果是正式项目,我们需要从数据库数据数据,然后再校验,形式如下:
// User user = userDAO.query(username);
if (!username.equals(uname)) {
throw new UsernameNotFoundException(username);
}
// 封装成 Spring security 定义的 User 对象
return User.builder()
.username(username)
.passwordEncoder(s -> passwordEncoder.encode(passwd))
.authorities(new SimpleGrantedAuthority("user"))
.build();
}
}
上面代码实现,这里主要在内存固定用户名与密码,真实环境下,我们需要修改成从数据库查询用户信息。
接着我们需要把 UserServiceImpl 配置到 Spring security 中。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserServiceImpl userService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 使用自定义用户服务校验登录信息
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 用户登录信息校验使用自定义 userService
// 还需要注意密码加密与验证需要使用同一种方式
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
}
上面的配置中,密码部分我们使用 BCrypt 算法加密,这里需要注意,加密与解密需要使用同一种方式。
接着我们需要实现一个自定义的登陆页面,这里就懒得自己写了,直接使用 spring-session-data-redis 页面。
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect"
layout:decorate="~{layout}">
<head>
<title>Login</title>
</head>
<body>
<div layout:fragment="content">
<!-- 自定义登录的请求 -->
<form name="f" th:action="@{/auth/login}" method="post">
<fieldset>
<legend>Please Login -</legend>
<div th:if="${param.error}" class="alert alert-error">Invalid username and password.</div>
<div th:if="${param.logout}" class="alert alert-success">You have been logged out.</div>
<label for="username">Username</label>
<input type="text" id="username" name="username"/>
<label for="password">Password</label>
<input type="password" id="password" name="password"/>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<label>remember me: </label>
<input type="checkbox" name="remember-me"/>
<div class="form-actions">
<button type="submit" class="btn">Log in</button>
</div>
</fieldset>
</form>
</div>
</body>
</html>
这里需要注意一点,这里 form 表单的请求地址使用 /auth/login,我们需要在下面配置中修改,默认情况下登录请求的地址需要为 /login。
接着我们在上面的 SecurityConfig 类增加相应配置方法:
/**
* 自定义处理登录处理
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests((authorize) -> authorize
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 静态资源,比如 css,js 无需登录鉴权
.anyRequest().permitAll() // 其他页面需要登录鉴权
).formLogin((formLogin) -> formLogin // 自定义登录页面
.loginPage("/login") // 登录页
.loginProcessingUrl("/auth/login") // 自定义登录请求地址
.permitAll()// 登录页当然无需鉴权了,不然不就套娃了吗?
).logout(LogoutConfigurer::permitAll // 登出页面
).rememberMe(rememberMe -> rememberMe
.rememberMeCookieName("test-remember") // 自定义记住我 cookie 名
.key("test") // 盐值
.tokenValiditySeconds(3600 * 12)) // 记住我,本地生成 cookie 包含用户信息
;
}
这个方法可能比较长,重点解释一下:
authorizeRequests方法内需要指定那些页面需要鉴权,这里我们指定静态资源无需登录鉴权,其他请求我们都需要登录鉴权formLogin方法内修改默认的登录页面地址,以及登录的请求地址。logout在这里面可以配置登出的相关配置。rememberMe开启这个功能之后,当内部 Session 过期之后,用户还可以根据用户浏览器中的 Cookie 信息实现免登录的功能。
最后我们需要配置一些页面的跳转地址:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 首页
registry.addViewController("/").setViewName("home");
// 登录之后跳转到 home 页
registry.addViewController("/login").setViewName("login");
}
}
总结
到此为止,我们已经集成 Spring-Session 与 Spring-security 完成完整的网站的登录鉴权功能。从这个例子可以看到,引入这个两个框架之后,我们只需要按照 Spring 规范开发即可,其他复杂实现原理我们都不需要自己实现了,这样真的很方便。
上面只是一个简单的小例子,小黑只是抛转引玉一下,真实开发中可能需要修改配置会更多,这里需要使用小伙伴自己在深入研究了。
参考
- https://creaink.github.io/post/Backend/SpringBoot/Spring-boot-security.html
- https://github.com/spring-projects/spring-session
欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:studyidea.cn
实战开发,使用 Spring Session 与 Spring security 完成网站登录改造!!的更多相关文章
- 分享Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站
这是个什么的项目? 使用 Node.js + Koa2 + MySQL + Vue.js 实战开发一套完整个人博客项目网站. 博客线上地址:www.boblog.com Github地址:https: ...
- spring session 和 spring security整合
背景: 我要做的系统前面放置zuul. 使用自己公司提供的单点登录服务.后面的业务应用也是spring boot支撑的rest服务. 目标: 使用spring security管理权限包括权限.用户请 ...
- 使用Spring Session实现Spring Boot水平扩展
小编说:本文使用Spring Session实现了Spring Boot水平扩展,每个Spring Boot应用与其他水平扩展的Spring Boot一样,都能处理用户请求.如果宕机,Nginx会将请 ...
- spring session实现同域下单点登录
Session会话管理 在Web项目开发中,Session会话管理是一个很重要的部分,用于存储与记录用户的状态或相关的数据:通常情况下session交由容器(tomcat)来负责存储和管理,但是如果项 ...
- spring boot系列03--spring security (基于数据库)登录和权限控制(下)
(接上篇) 后台 先说一下AuthConfig.java Spring Security的主要配置文件之一 AuthConfig 1 @Configuration 2 @EnableWebSecuri ...
- spring boot系列03--spring security (基于数据库)登录和权限控制(上)
这篇打算写一下登陆权限验证相关 说起来也都是泪,之前涉及权限的比较少所以这次准备起来就比较困难. 踩了好几个大坑,还好最终都一一消化掉(这是废话你没解决你写个什么劲
- 别再让你的微服务裸奔了,基于 Spring Session & Spring Security 微服务权限控制
微服务架构 网关:路由用户请求到指定服务,转发前端 Cookie 中包含的 Session 信息: 用户服务:用户登录认证(Authentication),用户授权(Authority),用户管理(R ...
- Re:从零开始的Spring Session(二)
上一篇文章介绍了一些Session和Cookie的基础知识,这篇文章开始正式介绍Spring Session是如何对传统的Session进行改造的.官网这么介绍Spring Session: Spri ...
- Re:从零开始的Spring Session(一)
Session和Cookie这两个概念,在学习java web开发之初,大多数人就已经接触过了.最近在研究跨域单点登录的实现时,发现对于Session和Cookie的了解,并不是很深入,所以打算写两篇 ...
随机推荐
- 如何理解Flutter中的asyc 和 await
https://blog.csdn.net/xdhc304/article/details/90232723 Flutter的语法非常精简, 对于异步任务, 只要使用asyc和awai 配合就能实现, ...
- Redis(二)Jedis操作Redis
如果测试连接的时候,报下面这个异常,可以参考下面的博客进行处理: Exception in thread "main" redis.clients.jedis.exceptions ...
- 第四课 OOP封装继承多态解析,接口抽象类选择 2019-04-21
父类 xx = new 子类(); xx.method(); 1 普通方法由编译时决定(左边) --- 提高效率 2 虚方法(virtual) 由运行时决定-- -多态,灵活 3 抽象方法由运行时决 ...
- Raid0,1,5,10,50
raid0 就是把多个硬盘合并成1个逻辑盘使用,数据读写时对各硬盘同时操作,不同硬盘写入不同数据,速度快. **最少需要2块硬盘 raid1 同时对2个硬盘读写(同样的数据).强调数据的安全性.损坏一 ...
- Python之自定义函数
函数 1.定义函数 在Python中定义一个函数要使用def语句,一次写出函数名.括号.括号中的的参数和冒号,然后在缩进块中编写函数体,函数的返回值用return返回.如下所示: def 函数名(参数 ...
- Qt子类化后qss设置背景色无效的问题
1.问题背景 在某个类中,用到了一个组合的widget,有按钮进度条等,类似于视频播放器按钮控制区和精度条(参考了很多feiyangqingyun的文章,感谢),调试正常后整理代码,为了提高代码可读性 ...
- timeit_list操作测试
''' timeit库Timer函数 ''' from timeit import Timer def test1(): l = list(range(1000)) def test2(): l = ...
- 10-9 重要的内置函数(zip、filter、map、sorted)
reverse----reversed l = [1,2,3,4,5,6] l.reverse() #不会保留原列表 print(l) l =[1,2,3,4,5,6] l2 = reversed(l ...
- MediaStreamConstraints对象
MediaStreamConstraints对象作用是在调用getUserMedia()时用于指定应在返回的MediaStream中包括哪些轨道,以及(可选)为这些轨道的设置约束. 属性 audio布 ...
- windows:shellcode 远程线程hook/注入(三)
今天介绍第三种远程执行shellcode的思路:函数回调: 1.所谓回调,简单理解: windows出厂时,内部有很多事务的处理无法固化(无法100%预料外部会遇到哪些情况),只能留下一堆的接口,让开 ...