在一次的web项目开发中,初期用了公司的负载均衡,后台2台服务器,用了会话保持,所以在用户登录后没有问题。后来更换了域名和主体,没法实现会话保持,改成了springsession,记录下来操作。
一、加入依赖
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
二、配置redis
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.session.data.redis.config.ConfigureRedisAction;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import redis.clients.jedis.JedisPoolConfig;
/**
* redis集群JEDIS配置
* redis在发送的时候,也采用@Retryable进行重试,因为公司有防火墙
*/
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) //启用springsession
public class RedisConfig {
@Value("${redis.address:ip:port,ip:port,ip:port}")
private String redisAddress;
@Value("${redis.timeout:5000}")
private int timeout;
@Value("${common.redis.password:pwd}")
private String password;
@Value("${redis.maxRedirects:5}")
private int maxRedirects;
@Value("${redis.minIdle:5}")
private int minIdle;
@Value("${redis.maxIdle:50}")
private int maxIdle;
@Value("${redis.maxTotal:200}")
private int maxTotal;
@Value("${redis.maxWaitMillis:100000}")
private int maxWaitMillis;
@Value("${redis.ssl:false}")
private boolean ssl = false;
@Bean(value = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean(value = "stringRedisTemplate")
public StringRedisTemplate stringRedisTemplate(
@Qualifier("jedisConnectionFactory") RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean(value = "jedisConnectionFactory")
public JedisConnectionFactory jedisConnectionFactory() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration();
redisClusterConfiguration.setClusterNodes(getRedisTemplateAddress(redisAddress));
redisClusterConfiguration.setMaxRedirects(maxRedirects);
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(redisClusterConfiguration,
jedisPoolConfig());
jedisConnectionFactory.setTimeout(timeout);
// 设置密码
jedisConnectionFactory.setPassword(password);
if (ssl) {
jedisConnectionFactory.setUseSsl(ssl);
}
return jedisConnectionFactory;
}
private JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(maxTotal);
jedisPoolConfig.setMinIdle(minIdle);
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
return jedisPoolConfig;
}
private Set<RedisNode> getRedisTemplateAddress(String s) {
Set<RedisNode> nodes = new HashSet<>();
String regex = "(?:\\s|,)+";
for (String hoststuff : s.split(regex)) {
if ("".equals(hoststuff)) {
continue;
}
int finalColon = hoststuff.lastIndexOf(':');
if (finalColon < 1) {
throw new IllegalArgumentException("Invalid server ``" + hoststuff + "'' in list: " + s);
}
String hostPart = hoststuff.substring(0, finalColon);
String portNum = hoststuff.substring(finalColon + 1);
nodes.add(new RedisNode(hostPart, Integer.parseInt(portNum)));
}
return nodes;
}
//阿里云启用httpsession需要加上下面配置
@Bean
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
}
三、重写SpringSecurity的session注册器
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@Component
public class MySessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {
private static final String SESSIONIDS = "sessionIds";
private static final String PRINCIPALS = "principals";
@Autowired
private RedisTemplate redisTemplate;
private static final Logger logger = LoggerFactory.getLogger(MySessionRegistryImpl.class);
// private final ConcurrentMap<Object, Set<String>> principals = new
// ConcurrentHashMap();
// private final Map<String, SessionInformation> sessionIds = new
// ConcurrentHashMap();
public MySessionRegistryImpl() {
}
@Override
public List<Object> getAllPrincipals() {
return new ArrayList(this.getPrincipalsKeySet());
}
@Override
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = this.getPrincipals(((UserDetails) principal).getUsername());
if (sessionsUsedByPrincipal == null) {
return Collections.emptyList();
} else {
List<SessionInformation> list = new ArrayList(sessionsUsedByPrincipal.size());
Iterator var5 = sessionsUsedByPrincipal.iterator();
while (true) {
SessionInformation sessionInformation;
do {
do {
if (!var5.hasNext()) {
return list;
}
String sessionId = (String) var5.next();
sessionInformation = this.getSessionInformation(sessionId);
} while (sessionInformation == null);
} while (!includeExpiredSessions && sessionInformation.isExpired());
list.add(sessionInformation);
}
}
}
@Override
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
return this.getSessionInfo(sessionId);
}
@Override
public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
this.removeSessionInformation(sessionId);
}
@Override
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
@Override
public void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Registering session " + sessionId + ", for principal " + principal);
}
if (this.getSessionInformation(sessionId) != null) {
this.removeSessionInformation(sessionId);
}
this.addSessionInfo(sessionId, new SessionInformation(principal, sessionId, new Date()));
// this.sessionIds.put(sessionId, new SessionInformation(principal,
// sessionId, new Date()));
Set<String> sessionsUsedByPrincipal = this.getPrincipals(principal.toString());
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet();
Set<String> prevSessionsUsedByPrincipal = this.putIfAbsentPrincipals(principal.toString(),
sessionsUsedByPrincipal);
if (prevSessionsUsedByPrincipal != null) {
sessionsUsedByPrincipal = prevSessionsUsedByPrincipal;
}
}
((Set) sessionsUsedByPrincipal).add(sessionId);
this.putPrincipals(principal.toString(), sessionsUsedByPrincipal);
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal);
}
}
@Override
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = this.getSessionInformation(sessionId);
if (info != null) {
if (this.logger.isTraceEnabled()) {
this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
}
this.removeSessionInfo(sessionId);
Set<String> sessionsUsedByPrincipal = this.getPrincipals(info.getPrincipal().toString());
if (sessionsUsedByPrincipal != null) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");
}
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.isEmpty()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Removing principal " + info.getPrincipal() + " from registry");
}
this.removePrincipal(((UserDetails) info.getPrincipal()).getUsername());
}
if (this.logger.isTraceEnabled()) {
this.logger.trace("Sessions used by '" + info.getPrincipal() + "' : " + sessionsUsedByPrincipal);
}
}
}
}
public void addSessionInfo(final String sessionId, final SessionInformation sessionInformation) {
BoundHashOperations<String, String, SessionInformation> hashOperations = redisTemplate.boundHashOps(SESSIONIDS);
hashOperations.put(sessionId, sessionInformation);
}
public SessionInformation getSessionInfo(final String sessionId) {
BoundHashOperations<String, String, SessionInformation> hashOperations = redisTemplate.boundHashOps(SESSIONIDS);
return hashOperations.get(sessionId);
}
public void removeSessionInfo(final String sessionId) {
BoundHashOperations<String, String, SessionInformation> hashOperations = redisTemplate.boundHashOps(SESSIONIDS);
hashOperations.delete(sessionId);
}
public Set<String> putIfAbsentPrincipals(final String key, final Set<String> set) {
BoundHashOperations<String, String, Set<String>> hashOperations = redisTemplate.boundHashOps(PRINCIPALS);
hashOperations.putIfAbsent(key, set);
return hashOperations.get(key);
}
public void putPrincipals(final String key, final Set<String> set) {
BoundHashOperations<String, String, Set<String>> hashOperations = redisTemplate.boundHashOps(PRINCIPALS);
hashOperations.put(key, set);
}
public Set<String> getPrincipals(final String key) {
BoundHashOperations<String, String, Set<String>> hashOperations = redisTemplate.boundHashOps(PRINCIPALS);
return hashOperations.get(key);
}
public Set<String> getPrincipalsKeySet() {
BoundHashOperations<String, String, Set<String>> hashOperations = redisTemplate.boundHashOps(PRINCIPALS);
return hashOperations.keys();
}
public void removePrincipal(final String key) {
BoundHashOperations<String, String, Set<String>> hashOperations = redisTemplate.boundHashOps(PRINCIPALS);
hashOperations.delete(key);
}
}
四、WebSecurityConfigurerAdapter配置
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.PortMapper;
import org.springframework.security.web.PortMapperImpl;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.util.RedirectUrlBuilder;
import org.springframework.security.web.util.UrlUtils;
import com.xxx.security.MchCustomUserDetailService;
import com.xxx.security.MyPasswordEncoder;
import com.xxx.security.PamchFilterSecurityInterceptor;
@Configuration
@EnableGlobalMethodSecurity
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MchCustomUserDetailService mchUserDetailService;
@Autowired
private PamchFilterSecurityInterceptor loginAuthInterceptor;
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthorizedEntryPoint())
.and()
.csrf()
.disable()
// 登录验证
.authorizeRequests()
.antMatchers("/login/**", "/login**", "/visitWeb", "/smct/idiomJcaptchaService*",
"/smct/charJcaptchaService*", "/smct/arithJcaptchaService*", "/operatorinfo/*", "/security/*",
"/static/*", "/merchantRequest", "/merchantRequest/*", "/onlinePay/obcAsyncReturn",
"/ecmsContract/notify", "/contract**", "/ecmsCustomerCon/*", "/ecmsContractCon/*",
"/ecmsTemplateCon/*", "/monitorWeb", "/api/**", "/front/**").permitAll()
// 任何请求,登录后可以访问
.anyRequest().authenticated()
// 登录
.and().formLogin().loginPage("/login").loginProcessingUrl("/security/j_spring_security_check")
.successHandler(loginSuccessHandler()).failureHandler(loginFailureHandler()).permitAll()
// 登出
.and().logout().logoutSuccessHandler(logoutSuccessHandler()).permitAll()// 跨域
//session管理 .and().sessionManagement().maximumSessions(1).sessionRegistry(mySessionRegistry());
// 过滤器
// 过验证码
http.addFilterBefore(loginConfirmCodeFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(loginAuthInterceptor, FilterSecurityInterceptor.class);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/statics/**", "/common/*");
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 登录用户的注册认证密码加密
auth.authenticationProvider(daoAuthenticationProvider());
// auth.userDetailsService(mchUserDetailService).passwordEncoder(passwordEncoder());
}
@Bean("authenticationManager")
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 判断是否需要加判断是否需要加入虚拟路径
*/
@Value("${security.contextRelative:false}")
private boolean contextRelative;
/**
* 用来开启是否启动强制https跳转
*/
@Value("${security.forceHttps:false}")
private boolean forceHttps;
/***
* 登录成功
*
* @return
*/
private AuthenticationSuccessHandler loginSuccessHandler() {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setDefaultTargetUrl("/index");
successHandler.setAlwaysUseDefaultTargetUrl(true);
successHandler.setRedirectStrategy(new SecurityRedirectStrategy(contextRelative, forceHttps));
return successHandler;
}
/***
* 登录失败
*/
private AuthenticationFailureHandler loginFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandle = new SimpleUrlAuthenticationFailureHandler();
failureHandle.setDefaultFailureUrl("/login");
failureHandle.setRedirectStrategy(new SecurityRedirectStrategy(contextRelative, forceHttps));
return failureHandle;
}
/***
* 登出成功
*
* @return
*/
private LogoutSuccessHandler logoutSuccessHandler() {
SimpleUrlLogoutSuccessHandler simpleUrlLogoutSuccessHandler = new SimpleUrlLogoutSuccessHandler();
simpleUrlLogoutSuccessHandler.setDefaultTargetUrl("/login");
simpleUrlLogoutSuccessHandler.setRedirectStrategy(new SecurityRedirectStrategy(contextRelative, forceHttps));
return simpleUrlLogoutSuccessHandler;
}
/***
* 登录确认失败
*
* @return
*/
private AuthenticationFailureHandler loginConfirmFailureHandler() {
SimpleUrlAuthenticationFailureHandler loginConfirmFailureHandler = new SimpleUrlAuthenticationFailureHandler();
loginConfirmFailureHandler.setDefaultFailureUrl("/login");
loginConfirmFailureHandler.setRedirectStrategy(new SecurityRedirectStrategy(contextRelative, forceHttps));
return loginConfirmFailureHandler;
}
@Bean
public LoginConfirmCodeFilter loginConfirmCodeFilter() {
LoginConfirmCodeFilter filter = new LoginConfirmCodeFilter();
filter.setFilterProcessesUrl("/security/j_spring_security_check");
filter.setPostOnly(true);
filter.setAuthenticationFailureHandler(loginConfirmFailureHandler());
return filter;
}
@Bean(name = "daoAuthenticationProvider")
public AuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(mchUserDetailService);
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
@Bean
public MyPasswordEncoder passwordEncoder() {
return new MyPasswordEncoder();
}
@Bean
public MySessionRegistryImpl mySessionRegistry() {
return new MySessionRegistryImpl();
}
/**
* 用来处理security中https跳转
*/
class SecurityRedirectStrategy implements RedirectStrategy {
private PortMapper portMapper = new PortMapperImpl();
private boolean contextRelative;
private boolean forceHttps;
public SecurityRedirectStrategy(boolean contextRelative, boolean forceHttps) {
this.contextRelative = contextRelative;
this.forceHttps = forceHttps;
}
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (forceHttps) {
RedirectUrlBuilder urlBuilder = new RedirectUrlBuilder();
urlBuilder.setScheme("https");
urlBuilder.setServerName(request.getServerName());
Integer httpsPort = portMapper.lookupHttpsPort(request.getServerPort());
urlBuilder.setPort(httpsPort);
urlBuilder.setContextPath(request.getContextPath());
urlBuilder.setServletPath(url);
urlBuilder.setPathInfo(request.getPathInfo());
urlBuilder.setQuery(request.getQueryString());
redirectUrl = urlBuilder.getUrl();
}
response.sendRedirect(redirectUrl);
}
private String calculateRedirectUrl(String contextPath, String url) {
if (!UrlUtils.isAbsoluteUrl(url)) {
if (contextRelative) {
return url;
} else {
return contextPath + url;
}
}
if (!contextRelative) {
return url;
}
url = url.substring(url.lastIndexOf("://") + 3);
url = url.substring(url.indexOf(contextPath) + contextPath.length());
if (url.length() > 1 && url.charAt(0) == '/') {
url = url.substring(1);
}
return url;
}
}
}
五、测试
1、下载ngnix windows版本,解压到本地
2、修改ngnix.conf文件如下:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
#后台服务的ip和端口
upstream tomcat{
server ip1:port weight=1;
server ip2:port weight=1;
}
server {
listen 8081;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
proxy_pass http://tomcat;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
3、启动ngnix
docs进入到ngnix根目录
--启动命令
D:\Program\nginx-1.12.2>start nginx
--停止命令
D:\Program\nginx-1.12.2>nginx -s stop
启动后访问下面端口,跳转到ngnix主页,说明成功
在访问项目路径
http://localhost:8081/projectname/login
登录成功并且能2台服务器都可以接受到请求,说明成功。