1. 新建Maven项目 cluster-session
2. pom.xml
4.0.0 com.java cluster-session 1.0.0 org.springframework.boot spring-boot-starter-parent 2.0.5.RELEASE org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-data-redis org.springframework.session spring-session-data-redis org.springframework.cloud spring-cloud-starter-oauth2 2.0.0.RELEASE org.springframework.session spring-session 1.3.5.RELEASE org.springframework springloaded 1.2.8.RELEASE provided org.springframework.boot spring-boot-devtools provided ${project.artifactId} org.apache.maven.plugins maven-compiler-plugin 1.8 1.8 UTF-8 org.springframework.boot spring-boot-maven-plugin repackage
3. ClusterSessionStarter.java
package com.java;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;/** ** * @author Logan * */@SpringBootApplicationpublic class ClusterSessionStarter { public static void main(String[] args) { SpringApplication.run(ClusterSessionStarter.class, args); }}* * 主启动类 * *
4. SessionInformationExpiredStrategyImpl.java
package com.java.session;import java.io.IOException;import javax.servlet.ServletException;import javax.utils.ResponseUtils;import org.springframework.security.web.session.SessionInformationExpiredEvent;import org.springframework.security.web.session.SessionInformationExpiredStrategy;/** * Session过期处理策略 * * @author Logan * @createDate 2019-02-14 * @version 1.0.0 * */public class SessionInformationExpiredStrategyImpl implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { ResponseUtils.write(event.getResponse(), "你的账号在另一地点被登录"); }}
5. ApplicationContextConfig.java
package com.java.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;/** * 配置文件类 * * @author Logan * @createDate 2019-02-14 * @version 1.0.0 * */@Configuration@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)public class ApplicationContextConfig { /** * 配置密码编码器,Spring Security 5.X必须配置,否则登录时报空指针异常 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}
6. LoginConfig.java
package com.java.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import com.java.session.SessionInformationExpiredStrategyImpl;/** * 登录相关配置 * * @author Logan * @createDate 2019-02-14 * @version 1.0.0 * */@Configurationpublic class LoginConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 设置不需要授权的请求 .antMatchers("/js/*", "/login.html").permitAll() // 其它任何请求都需要验证权限 .anyRequest().authenticated() // 设置自定义表单登录页面 .and().formLogin().loginPage("/login.html") // 设置登录验证请求地址为自定义登录页配置action ("/login/form") .loginProcessingUrl("/login/form") // 设置默认登录成功跳转页面 .defaultSuccessUrl("/main.html") /* session 管理 */ .and().sessionManagement() // 设置Session失效跳转页面 .invalidSessionUrl("/login.html") // 设置最大Session数为1 .maximumSessions(1) // 设置Session过期处理策略 .expiredSessionStrategy(new SessionInformationExpiredStrategyImpl()).and() // 暂时停用csrf,否则会影响验证 .and().csrf().disable(); }}
7. SecurityUserDetailsService.java
package com.java.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.stereotype.Component;/** * UserDetailsService实现类 * * @author Logan * @createDate 2019-02-14 * @version 1.0.0 * */@Componentpublic class SecurityUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 数据库存储密码为加密后的密文(明文为123456) String password = passwordEncoder.encode("123456"); System.out.println("username: " + username); System.out.println("password: " + password); // 模拟查询数据库,获取属于Admin和Normal角色的用户 User user = new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("Admin,Normal")); return user; }}
8. ResponseUtils.java
package javax.utils;import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.OutputStream;import java.io.PrintWriter;import java.net.URLEncoder;import javax.servlet.http.HttpServletResponse;import com.fasterxml.jackson.databind.ObjectMapper;/** * HTTP 输出响应内容工具类 * * @author Logan * @createDate 2019-02-14 * @version 1.0.0 * */public class ResponseUtils { /** * 发送HTTP响应信息 * * @param response HTTP响应对象 * @param message 信息内容 * @throws IOException 抛出异常,由调用者捕获处理 */ public static void write(HttpServletResponse response, String message) throws IOException { response.setContentType("text/html;charset=UTF-8"); try ( PrintWriter writer = response.getWriter(); ) { writer.write(message); writer.flush(); } } /** * 发送HTTP响应信息,JSON格式 * * @param response HTTP响应对象 * @param message 输出对象 * @throws IOException 抛出异常,由调用者捕获处理 */ public static void write(HttpServletResponse response, Object message) throws IOException { response.setContentType("application/json;charset=UTF-8"); ObjectMapper mapper = new ObjectMapper(); try ( PrintWriter writer = response.getWriter(); ) { writer.write(mapper.writeValueAsString(message)); writer.flush(); } } /** * 下载文件 * * @param response HTTP响应对象 * @param message 输出对象 * @throws IOException 抛出异常,由调用者捕获处理 */ public static void write(HttpServletResponse response, File file) throws IOException { String fileName = file.getName(); try ( OutputStream out = response.getOutputStream(); FileInputStream in = new FileInputStream(file); ) { // 对文件名进行URL转义,防止中文乱码 fileName = URLEncoder.encode(fileName, "UTF-8"); // 空格用URLEncoder.encode转义后会变成"+",所以要替换成"%20",浏览器会解码回空格 fileName = fileName.replace("+", "%20"); // "+"用URLEncoder.encode转义后会变成"%2B",所以要替换成"+",浏览器不对"+"进行解码 fileName = fileName.replace("%2B", "+"); response.setContentType("application/x-msdownload;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=" + fileName); byte[] bytes = new byte[4096]; int len = -1; while ((len = in.read(bytes)) != -1) { out.write(bytes, 0, len); } out.flush(); } }}
9. application.properties
server.port=8080server.servlet.session.timeout=600spring.session.store-type=redis# REDIS (RedisProperties)# Redis数据库索引(默认为0)spring.redis.database=0# Redis服务器地址spring.redis.host=192.168.32.10# Redis服务器连接端口spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=redis123.# 连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=10# 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1# 连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=5# 连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0# 连接超时时间(毫秒)spring.redis.timeout=10000spring.cache.redis.time-to-live=600
10. src/main/resources 下静态资源文件如下:
static/login.html
static/main.html
11. login.html
登录 用户自定义登录页面
12. main.html
首页 Designed by Logan.
.