Spring Security 之集群Session配置
1. 新建Maven项目 cluster-session
2. pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.java</groupId> <artifactId>cluster-session</artifactId> <version>1.0.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> </parent> <dependencies> <!-- Spring Boot --> <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> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <version>2.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.3.5.RELEASE</version> </dependency> <!-- 热部署 --> <dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> <version>1.2.8.RELEASE</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
3. ClusterSessionStarter.java
package com.java; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * <blockquote><pre> * * 主启动类 * * </pre></blockquote> * * @author Logan * */ @SpringBootApplication public 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 * */ @Configuration public 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 * */ @Component public 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=8080
server.servlet.session.timeout=600
spring.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=10000
spring.cache.redis.time-to-live=600
10. src/main/resources 下静态资源文件如下:
static/login.html
static/main.html
11. login.html
<!DOCTYPE html> <html> <head> <title>登录</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> </head> <body> <!--登录框--> <div align="center"> <h2>用户自定义登录页面</h2> <fieldset style="width: 300px;"> <legend>登录框</legend> <form action="/login/form" method="post"> <table> <tr> <th>用户名:</th> <td><input name="username" value="Logan" /> </td> </tr> <tr> <th>密码:</th> <td><input type="password" name="password" value="123456" /> </td> </tr> <tr> <th></th> <td></td> </tr> <tr> <td colspan="2" align="center"><button type="submit">登录</button></td> </tr> </table> </form> </fieldset> </div> </body> </html>
12. main.html
<html> <head> <title>首页</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <script> window.onclick = function() { window.open("http://www.cnblogs.com/jonban/"); } </script> </head> <body style="background-color: cyan;text-align: center;"> <h1><span style="text-align:center;color:purple;cursor: pointer;">Designed by Logan.</span></h1> <canvas id="c"></canvas> <script> var b = document.body; var c = document.getElementsByTagName('canvas')[0]; var a = c.getContext('2d'); document.body.clientWidth; </script> <script> with(m = Math) C = cos, S = sin, P = pow, R = random; c.width = c.height = f = 613; h = -250; function p(a, b, c) { if(c > 60) return [S(a * 7) * (13 + 5 / (.2 + P(b * 4, 4))) - S(b) * 50, b * f + 50, 625 + C(a * 7) * (13 + 5 / (.2 + P(b * 4, 4))) + b * 400, a * 1 - b / 2, a ]; A = a * 2 - 1; B = b * 2 - 1; if(A * A + B * B < 1) { if(c > 37) { n = (j = c & 1) ? 6 : 4; o = .5 / (a + .01) + C(b * 125) * 3 - a * 300; w = b * h; return [o * C(n) + w * S(n) + j * 610 - 390, o * S(n) - w * C(n) + 550 - j * 350, 1180 + C(B + A) * 99 - j * 300, .4 - a * .1 + P(1 - B * B, -h * 6) * .15 - a * b * .4 + C(a + b) / 5 + P(C((o * (a + 1) + (B > 0 ? w : -w)) / 25), 30) * .1 * (1 - B * B), o / 1e3 + .7 - o * w * 3e-6] } if(c > 32) { c = c * 1.16 - .15; o = a * 45 - 20; w = b * b * h; z = o * S(c) + w * C(c) + 620; return [o * C(c) - w * S(c), 28 + C(B * .5) * 99 - b * b * b * 60 - z / 2 - h, z, (b * b * .3 + P((1 - (A * A)), 7) * .15 + .3) * b, b * .7] } o = A * (2 - b) * (80 - c * 2); w = 99 - C(A) * 120 - C(b) * (-h - c * 4.9) + C(P(1 - b, 7)) * 50 + c * 2; z = o * S(c) + w * C(c) + 700; return [o * C(c) - w * S(c), B * 99 - C(P(b, 7)) * 50 - c / 3 - z / 1.35 + 450, z, (1 - b / 1.2) * .9 + a * .1, P((1 - b), 20) / 4 + .05] } } setInterval('for(i=0;i<1e4;i++)if(s=p(R(),R(),i%46/.74)){z=s[2];x=~~(s[0]*f/z-h);y=~~(s[1]*f/z-h);if(!m[q=y*f+x]|m[q]>z)m[q]=z,a.fillStyle="rgb("+~(s[3]*h)+","+~(s[4]*h)+","+~(s[3]*s[3]*-80)+")",a.fillRect(x,y,1,1)}', 0) </script> </body> </html>
.