spring webflux项目集成后台管理系统的用户登录,支持用户session
配置pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.disney.wdpro.service</groupId>
<artifactId>my-service</artifactId>
<version>9.4.2</version>
<packaging>war</packaging>
<name>my-service</name>
<properties>
<java.version>1.8</java.version>
<spring.boot.version>2.5.6</spring.boot.version>
<failOnMissingWebXml>false</failOnMissingWebXml>
</properties>
<dependencies>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>0.8.2.RELEASE</version>
</dependency>
<!-- commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<!-- eventbus, kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- other -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- for test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.7</version>
</dependency>
<!-- json -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.13.1</version>
</dependency>
<!-- qr code -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-client</artifactId>
<version>3.4.8</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
</dependencies>
<build>
<finalName>my-service</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>utf8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
启动类:
package com.my.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.data.redis.config.annotation.web.server.EnableRedisWebSession;
import org.springframework.web.reactive.config.EnableWebFlux;
@Slf4j
@SpringBootApplication
@EnableWebFlux
@EnableScheduling
@EnableRetry
@EnableRedisWebSession(maxInactiveIntervalInSeconds = 60*60*2, redisNamespace="user:login:seession:")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
用户登录成功后,把登录user放入WebSession对象:
package com.my.controller;
import com.my.BackstageResponseDto;
import com.my.LoginDto;
import com.my.BackstageButtonService;
import com.my.BackstageMenuService;
import com.my.BackstageUserService;
import com.my.ResponseDto;
import com.my.ResponseEnum;
import com.my.WebSessionConstant;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Slf4j
@Controller
@RequiredArgsConstructor
public class BackstageLoginController { // 后台登录类
final BackstageUserService backstageUserService;
final ReactiveRedisTemplate<String, String> stringReactiveRedisTemplate;
final BackstageMenuService backstageMenuService;
final BackstageButtonService backstageButtonService;
@Value("${my.url}")
String myUiUrl;
@RequestMapping("/backstage/login/myuser")
public Mono<Rendering> loginMyuser(@ModelAttribute Mono<LoginDto> loginModelMono, ServerWebExchange exchange) {
log.info("loginModelMono = {}", loginModelMono);
Map<String, String> dataMap = new HashMap<>();
return loginModelMono.defaultIfEmpty(LoginDto.builder().build())
.flatMap(loginDto -> {
try {
String loginUsername = "zhangsan";
return backstageUserService.validLoginUserAuthority(dataMap, loginUsername)
.flatMap(validMap -> {
Map.Entry<String, String> entry = validMap.entrySet().iterator().next();
String code = entry.getKey();
if (ResponseEnum.SUCCESS.getCode().equals(code)) {
String token = UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
log.info("valid login username {} is success, and generate a token for it. will redirect to backstage-ui = {}", myid, myUiUrl);
return stringReactiveRedisTemplate.opsForValue()
.set(token, myid, Duration.ofSeconds(180))
.thenReturn(Rendering.redirectTo(myUiUrl + "?token=" + token).build()); // refer https://gyoomi.blog.csdn.net/article/details/119735429?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2
}
String errorMsg = entry.getValue();
return buildLoginFailRendering(errorMsg);
})
.onErrorResume(throwable -> {
log.error("catch error when login myid {}", myid, throwable);
return buildLoginFailRendering("error: " + throwable.getMessage());
})
;
} catch (Exception e) {
log.error("catch error when solo return saml information", e);
return buildLoginFailRendering("error: " + e.getMessage());
}
});
}
private Mono<Rendering> buildLoginFailRendering(String errorMsg) {
try {
String uiUrl = myUiUrl + "?error=" + java.net.URLEncoder.encode(errorMsg, "UTF-8");
log.info("will redirect to backstage ui url = {}, which error msg = {}", uiUrl, errorMsg);
return Mono.just(Rendering.redirectTo(uiUrl).build())
.onErrorResume(throwable -> {
log.error("catch error when build backstage error msg: {}", errorMsg, throwable);
return Mono.just(Rendering.redirectTo(myUiUrl + "?error=systemUnExceptionError").build());
});
} catch (UnsupportedEncodingException e) {
log.error("error login backstage url", e);
return Mono.just(Rendering.redirectTo(myUiUrl + "?error=systemUnExceptionError").build());
}
}
@GetMapping("/backstage/login/token")
@ResponseBody
public Mono<BackstageResponseDto> token(String token, WebSession webSession) {
log.info("login by token {} from myUiUrl = {}, webSessionId ====== {}", token, myUiUrl, webSession.getId());
if (StringUtils.isBlank(token)) {
return Mono.just(
BackstageResponseDto.builder()
.code(ResponseEnum.BACKSTAGE_LOGIN_FAIL.getCode())
.msg("token cannot be empty")
.build()
);
}
return stringReactiveRedisTemplate.opsForValue().get(token)
.flatMap(myid -> loadUser(myid, token, webSession))
.switchIfEmpty(Mono.defer(() -> Mono.just(
BackstageResponseDto.builder()
.code(ResponseEnum.BACKSTAGE_LOGIN_FAIL.getCode())
.msg("token is invalid")
.build()
)
))
.doFinally(signalType ->
stringReactiveRedisTemplate.opsForValue().delete(token)
.doOnSuccess(isDeleteToken -> {
log.info("delete token {} in redis is {}", token, isDeleteToken);
})
.doOnError(throwable -> {
log.info("catch error when delete token {} in redis", token);
})
.subscribe()
);
}
private Mono<BackstageResponseDto> loadUser(String myid, String token, WebSession webSession) {
return backstageUserService.getUserByUsername(myid)
.flatMap(userModel -> {
log.info("load user {} from db is success, model = {}", myid, userModel);
webSession.getAttributes().put(WebSessionConstant.SESSION_LOGIN_USER, userModel);
return backstageMenuService.listMenuAuthorityByUsername(myid)
.flatMap(menuList -> {
log.info("load user {} menu authority list from db is success. menu list = {}", myid, menuList);
webSession.getAttributes().put(WebSessionConstant.SESSION_MENU_AUTHORITY, menuList);
return backstageButtonService.listButtonAuthorithByUsername(myid)
.map(buttonList -> {
log.info("load user {} button authority list from db is success. button list = {}", myid, buttonList);
webSession.getAttributes().put(WebSessionConstant.SESSION_BUTTON_AUTHORITY, buttonList);
return userModel;
});
})
.thenReturn(BackstageResponseDto.builder()
.code(ResponseEnum.SUCCESS.getCode())
.msg(webSession.getId())
.data(userModel)
.build())
.doOnNext(responseDto -> {
log.info("user login success with sessionId and return responseDto = {}", responseDto);
});
})
.doOnSuccess(backstageResponseDto -> {
log.info("login user {} by token {} is success", myid, token);
})
.onErrorResume(throwable -> {
log.error("catch error when login by myid {}", myid, throwable);
return Mono.just(BackstageResponseDto.FAIL(ResponseEnum.UNEXPECTED_ERROR.getCode(), throwable.getMessage()));
});
}
@RequestMapping("/backstage/login/user-not-login")
@ResponseBody
public BackstageResponseDto userNotLogin() {
log.info("build user not login BackstageResponseDto");
return BackstageResponseDto.builder()
.code(ResponseEnum.BACKSTAGE_USER_NOT_LOGIN.getCode())
.msg("user not login")
.build();
}
@GetMapping("/backstage/login/user-logout")
@ResponseBody
public Mono<BackstageResponseDto> logout(WebSession webSession) {
Object loginUserObj = webSession.getAttributes().get(WebSessionConstant.SESSION_LOGIN_USER);
webSession.getAttributes().remove(WebSessionConstant.SESSION_LOGIN_USER);
return webSession.invalidate()
.thenReturn(BackstageResponseDto.builder()
.code(ResponseEnum.SUCCESS.getCode())
.msg("user logout is success")
.build())
.doOnSuccess(v -> {
log.info("user logout success, user model = {}", loginUserObj);
})
.doOnError(throwable -> {
log.error("catch error when user logout, user model = {}", loginUserObj);
});
}
}
后台登录用户请求后台接口时,从WebSession类里获取登录用户:
@GetMapping(value = "/backstage/product/detail")
public Mono<BackstageResponseDto> queryProductDetail(String vid, WebSession webSession) {
UserModel loginUserModel = (UserModel) webSession.getAttribute(WebSessionConstant.SESSION_LOGIN_USER);
return null;
}
后台接口拦截filter类:
package com.my.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Map;
@Slf4j
@Component
public class BackstageFilter implements WebFilter {
@Value("${server.servlet.context-path}")
private String contextPath;
@Value("${backstage.filter.request.uri.local.enable:false}")
private Boolean backstageFilterRequestUriLocalEnable;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String requestContextPath = exchange.getRequest().getPath().contextPath().value();
String uri = exchange.getRequest().getURI().toString();
String urlPath = exchange.getRequest().getURI().getPath();
String requestPath = exchange.getRequest().getPath().toString();
String remoteAddress = exchange.getRequest().getRemoteAddress().toString();
if (!urlPath.contains("/backstage")
|| urlPath.contains("/backstage/login/username")
|| urlPath.contains("/backstage/login/token")
|| urlPath.contains("/backstage/login/user-not-login")
|| urlPath.contains("/backstage/login/user-logout")
) {
log.info("filter ignore url: {}, full path from uri = {}", urlPath, uri);
return chain.filter(exchange);
}
log.info("filter backstage authority url, requestContextPath = {}, uri = {}, urlPath = {}, requestPath = {}, RemoteAddress = {}", requestContextPath, uri, urlPath, requestPath, remoteAddress);
return exchange.getSession()
.flatMap(webSession -> {
Object obj = webSession.getAttributes().get(WebSessionConstant.SESSION_LOGIN_USER);
log.info("get BackstageFilter webSessionId === {}, session user obj = {}, uri = {}", webSession.getId(), obj, uri);
// if user not login
if (null == obj) {
ServerWebExchange.Builder serverWebExchange = exchange.mutate();
log.info("retrieve user is not login. BackstageFilter serverWebExchange = {}", serverWebExchange);
ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate()
.uri(URI.create(contextPath + "/backstage/login/user-not-login"))
.build();
if (backstageFilterRequestUriLocalEnable) {
serverHttpRequest = exchange.getRequest().mutate()
.uri(URI.create("/backstage/login/user-not-login"))
.build();
}
log.info("retrieve user is not login, request uri = {}, will redirect to {}", uri,
serverHttpRequest.getURI().toString());
return chain.filter(exchange.mutate().request(serverHttpRequest).build());
}
UserModel userModel = (UserModel) obj;
Map<String, Object> attributes = webSession.getAttributes();
log.info("filter login user {} ({}) to request url {}, sessionId = {}, menu authority list = {}, button authority list = {}, webSession.attributes = {}"
, userModel.getUsername(), userModel.getNickname(), urlPath, webSession.getId()
, webSession.getAttributes().get(WebSessionConstant.SESSION_MENU_AUTHORITY)
, webSession.getAttributes().get(WebSessionConstant.SESSION_BUTTON_AUTHORITY)
, attributes);
return chain.filter(exchange);
})
.doOnError(throwable -> {
log.error("catch error in BackstageFilter. uri = {}, urlPath = {}, requestPath = {}, RemoteAddress = {}", uri, urlPath, requestPath, remoteAddress, throwable);
});
}
private Mono<BackstageResponseDto> logout(WebSession webSession) {
Object loginUserObj = webSession.getAttributes().get(WebSessionConstant.SESSION_LOGIN_USER);
webSession.getAttributes().remove(WebSessionConstant.SESSION_LOGIN_USER);
return webSession.invalidate()
.thenReturn(BackstageResponseDto.builder()
.code(ResponseEnum.SUCCESS.getCode())
.msg("user logout is success")
.build())
.doOnSuccess(v -> {
log.info("user logout success, user model = {}", loginUserObj);
})
.doOnError(throwable -> {
log.error("catch error when user logout, user model = {}", loginUserObj);
});
}
}
end.