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.

posted on 2022-12-17 20:09  梦幻朵颜  阅读(565)  评论(0编辑  收藏  举报