场景

前面我们学习了springboot整合redis实现分布式session,对 spring session 有了一个最基本的认识。

有时候我们希望登陆的时候设置对应的 session 信息,然后结合拦截器进行是否登陆校验。

本文就给出一个结合拦截器使用的例子,让我们进一步感受下 spring session 的魅力吧。

实战

maven 依赖

  [xml]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>

类结构

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
│ Application.java │ ├─config │ WebConfig.java │ ├─controller │ ExampleController.java │ UserController.java │ ├─interceptor │ SessionInterceptor.java │ └─util SessionUtil.java

基本方法

  • Application.java
  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.github.houbb.spring.session.learn.boot.redis; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; /** * @author binbin.hou * @since 1.0.0 */ @SpringBootApplication @EnableRedisHttpSession public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }

通过 @EnableRedisHttpSession 注解启用 redis http session 的功能。

ExampleController.java 类和入门案例中一样,此处不再赘述。

  • SessionUtil.java

一个 session 构建的基础工具类。

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.github.houbb.spring.session.learn.boot.redis.util; /** * @author binbin.hou * @since 1.0.0 */ public final class SessionUtil { private SessionUtil(){} /** * session key 前缀 */ private static final String SESSION_KEY_PREFIX = "SESSION-KEY-"; public static String buildSessionKey(final String sessionId) { return SESSION_KEY_PREFIX + sessionId; } }

UserController.java

这里我们主要模拟一下用户的登陆和登出。

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.github.houbb.spring.session.learn.boot.redis.controller; import com.github.houbb.spring.session.learn.boot.redis.util.SessionUtil; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.util.UUID; @RestController @RequestMapping public class UserController { @RequestMapping("/loginIndex") public String loginIndex(HttpServletRequest req) { return "http://localhost:8080/login?username=guest"; } @RequestMapping("/login") public String login(HttpServletRequest req) { //1. 获取用户名密码 final String username = req.getParameter("username"); //2. 登陆校验 if("guest".equals(username)) { //3. 设置对应的信息 String sessionId = UUID.randomUUID().toString(); String sessionKey = SessionUtil.buildSessionKey(sessionId); // value 可以设置对应的权限值等等 String value = username; req.getSession().setAttribute(sessionKey, value); // 将 token 返回给前端 // 可以设置再页面的隐藏域中 return "sessionId: " + sessionId; } return "login failed"; } @RequestMapping("/logout") public String logout(HttpServletRequest req) { String sessionId = req.getParameter("sessionId"); req.getSession().removeAttribute(SessionUtil.buildSessionKey(sessionId)); return "登出成功"; } }

登陆首页

loginIndex 为登陆的首页,为了简单,我们直接显示需要请求的地址即可:

  [plaintext]
1
http://localhost:8080/login?username=guest

登陆

校验对应的 username,并且设置对应的 session 信息。

并且将这个 token 返回给前端,用户在登出的时候需要指定这个 token 做校验。

登出

清空对应的 session 信息。

拦截器

SessionInterceptor.java

这里我们是通过对应的 token 获取。

当然,对于 spring session 也可以直接根据 JSESSIONID 这种更加优雅的方式设置和获取。

如果信息不存在,则直接返回到登陆页面。

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.github.houbb.spring.session.learn.boot.redis.interceptor; import com.github.houbb.spring.session.learn.boot.redis.util.SessionUtil; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Writer; /** * @author binbin.hou * @since 1.0.0 */ @Component public class SessionInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { String sessionId = httpServletRequest.getParameter("sessionId"); if(StringUtils.isEmpty(sessionId)) { handleForbidden(httpServletRequest, httpServletResponse, "请重新登陆"); return false; } // 获取登陆信息 String sessionKey = SessionUtil.buildSessionKey(sessionId); String username = (String) httpServletRequest.getSession().getAttribute(sessionKey); if(StringUtils.isEmpty(username)) { handleForbidden(httpServletRequest, httpServletResponse, "请重新登陆"); return false; } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } private void handleForbidden(HttpServletRequest request, HttpServletResponse response, String info){ // 有这个值是异步请求,没有是同步请求。 String xRequest = request.getHeader("X-Requested-With"); if(!StringUtils.isEmpty(xRequest)){ //异步处理 response.setCharacterEncoding("UTF-8"); try { Writer writer = response.getWriter(); writer.write(info); response.setStatus(403); } catch (IOException e) { e.printStackTrace(); } }else{ try { // 可以跳转到登陆页面 request.getRequestDispatcher("loginIndex") .forward(request, response); } catch (ServletException | IOException e) { e.printStackTrace(); } } } }

拦截器设置

我们定义了拦截器,当然需要指定对应的生效范围:

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.github.houbb.spring.session.learn.boot.redis.config; import com.github.houbb.spring.session.learn.boot.redis.interceptor.SessionInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * @author binbin.hou * @since 1.0.0 */ @Configuration public class WebConfig extends WebMvcConfigurerAdapter { @Autowired private SessionInterceptor sessionInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(sessionInterceptor) .addPathPatterns("/**") .excludePathPatterns("/loginIndex") .excludePathPatterns("/login"); } @Bean public CharacterEncodingFilter initializeCharacterEncodingFilter() { CharacterEncodingFilter filter = new CharacterEncodingFilter(); filter.setEncoding("UTF-8"); filter.setForceEncoding(true); return filter; } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("forward:/loginIndex"); registry.setOrder(Ordered.HIGHEST_PRECEDENCE); super.addViewControllers(registry); } }

我们排除 /loginIndex/login 这两个请求地址的 session 拦截校验,当然也可以根据实际情况排除静态文件。

registry.addViewController("/").setViewName("forward:/loginIndex"); 这句话可以将默认的首页重定向到登陆页面。

测试验证

首页

浏览器访问 http://localhost:8080/

页面显示:

  [plaintext]
1
http://localhost:8080/login?username=guest

登陆

我们直接输入 http://localhost:8080/login?username=guest

页面返回对应的 token:

  [plaintext]
1
sessionId: 5251161a-d946-41e5-98fc-eae2e5c01960

登出

我们登出的时候,带上这个参数:http://localhost:8080/logout?sessionId=5251161a-d946-41e5-98fc-eae2e5c01960

页面提示:

  [plaintext]
1
登出成功

如果我们再次请求,页面就会跳转到登陆首页,因为这个时候的 session 已经被清空了。

小结

这一节我们展示了如何结合拦截器使用 spring session,这也是目前比较主流的做法。

对于很多讲解到这里一般就结束了,不过老马实际上是为了引入接下来的内容。

spring session 底层的实现原理是怎样的?

下一节将和各位小伙伴们一起学习下 spring session 的源码。

希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。

我是老马,期待与你的下次相遇。

参考资料

https://spring.io/projects/spring-session#overview

https://yq.aliyun.com/articles/371442

  • 实现原理

实现原理

Spring Session 内部实现原理(源码分析)