共计 7127 个字符,预计需要花费 18 分钟才能阅读完成。
上次小黑在文章中介绍了四种分布式一致性 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/an…
默认状况下,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/pos…
- https://github.com/spring-pro…
欢送关注我的公众号:程序通事,取得日常干货推送。如果您对我的专题内容感兴趣,也能够关注我的博客:studyidea.cn