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>

 

 

 

 

 

 

 

.

posted @ 2019-02-14 20:36  诚信天下  阅读(2203)  评论(0编辑  收藏  举报