前言

本文,演示Spring Boot Weflux集成SpringSecurity安全验证框架做项目的身份验证与权限管理;通过@EnableWebFluxSecurity注解加载与配置Security权限与用户登录信息,通过@EnableReactiveMethodSecurity注解在标准方法上用表达式模型做安全控制设置,如:使用@PreAuthorize注解在方法上控制用户访问RestAPI做权限验证;采用jwt做登录会话数字签名,通过jwt验证后,从jwt中获取用户唯一标识,在做相应业务验权和调用;

到此,本示例核心为完成一个基础springSecurity安全框架登录功能演示;

SpringWebFlux

SpringWebFlux是一套全新的springMVC框架体系下的新技术,随着SpingBoot的版本更新与微服务架构迭代,融入了更多的新技术,慢慢的让大众认识和学习了解到WebFlux的特点与设计,并接受在项目中实践,在springframework 5.0版本刚添加发布后,也存在一些学习与使用难点、甚至组件版本的兼容性等问题;随着版本的升级与迭代优化,现已有完全替代SpringWeb成为Web微服务开发框架主力军的潜力;

官方说明

SpringWebFlux是从spring framework 5.0版本开始在spring MVC整个生态框架中加入的,它是一套全新的ReactiveWeb技术,通够与现有的SpringMVC其它组件共存,是一个反应式堆栈Web框架,是全完全非阻塞,支持 Reactive Streams背压,并在 Netty、Undertow 和 Servlet 3.1+ 容器等服务器上运行。

在微服务架构中,您可以混合使用 Spring MVC 或 Spring WebFlux 控制器或 Spring WebFlux 功能端点的应用程序。在两个框架中都支持相同的基于注释的编程模型,可以更轻松地重用知识,同时还可以为正确的工作选择正确的工具。

并且从 Spring Web MVC 迁移到 Spring WebFlux 可以是无缝的,我们不需要重写原有的任何逻辑代码!

详细情况参见官方文档:

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html

SpringSecurity

Spring Security在spingMVC框架体系中用于做身份验证与权限管理的框架,增加系统的安全防护,防止非法登录与跨权限访问,目前很多项目中有应用;

官方说明

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实上的标准。

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。与所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义要求

特征

  • 对身份验证和授权的全面且可扩展的支持
  • 防止会话固定、点击劫持、跨站点请求伪造等攻击
  • Servlet API 集成
  • 与 Spring Web MVC 的可选集成

应用程序安全归结为两个或多或少独立的问题:身份验证(你是谁?)和授权(你被允许做什么?)。有时人们会说“访问控制”而不是“授权”,这可能会让人感到困惑,但这样想是有帮助的,因为“授权”在其他地方超载。Spring Security 的架构旨在将身份验证与授权分开,并为两者提供策略和扩展点。

详细情况参见官方文档:

https://spring.io/guides/topicals/spring-security-architecture

项目示例

创建一个叫webflux-demo的工程,用于本演示项目编写代码;

创建项目pom文件

pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.5</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--添加对日志系统的支持,采用最新的log4j2框架-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
    <!-- json解析包 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.75</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.10</version>
    </dependency>
    <!-- JWT Token验证机制 -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.8.1</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency>
</dependencies>

创建项目YML配置

application.yml

# 本地服务访问
server:
  # 服务端口
  port: 8080
  # 服务IP
  address: 0.0.0.0

spring:
  application:
    # 应用服务名
    name: security

# 配置日志
logging:
  level:
    com.netflix: info
    reactor.util: info
    org.springframework.boot: info
#    # log 级别
#    org.springframework.cloud.gateway: debug
  config: classpath:log4j2.xml

# 是否启用springboot的debug调试模式,会打印详细日志信息
debug: true

创建Log4j2日志配置

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
    status : 这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,会看到log4j2内部各种详细输出
    monitorInterval : Log4j能够自动检测修改配置文件和重新配置本身, 设置间隔秒数。
-->
<Configuration status="WARN" monitorInterval="600">
    <!-- 日志级别
            trace:追踪,就是程序推进一下,可以写个trace输出
            debug:调试,一般作为最低级别,trace基本不用。
            info:输出重要的信息,使用较多
            warn:警告,有些信息不是错误信息,但也要给一些提示。
            error:错误信息。用的也很多。
            fatal:致命错误。级别较高.
     -->
    <Appenders>
        <!--这个输出控制台的配置-->
        <Console name="Console" target="SYSTEM_OUT">
            <!-- 控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch),[DENY|ACCEPT] -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <!-- 输出日志的格式 -->
            <PatternLayout>
                <charset>UTF-8</charset>
                <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%thread] [%class{36}] [%L] - %msg%xEx%n</pattern>
            </PatternLayout>
        </Console>
    </Appenders>
    <Loggers>
        <!-- 配置日志的根节点 -->
        <root level="trace">
            <appender-ref ref="Console"/>
        </root>
        <!-- 第三方日志配置 -->
        <logger name="org.springframework.core" level="info"/>
        <logger name="org.springframework.beans" level="info"/>
        <logger name="org.springframework.context" level="info"/>
        <logger name="io.netty" level="warn"/>
        <logger name="org.apache.http" level="warn"/>
    </Loggers>
</Configuration>

创建启动类

JwtAppStart.java

package com.webflux.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
import reactor.core.publisher.Mono;

import java.security.Principal;
import java.util.HashMap;
import java.util.Map;

//注意:@EnableWebFlux用来配置freemarker,它适用于模板文件,但是对静态资源访问有问题,访问纯静态资源请注释
@EnableWebFlux
@RestController
@SpringBootApplication
//@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class JwtAppStart{

    /**
     此示例,只提供api级别的security接口较验
     测试流程:
     1.通过postamn 以get访问:http://127.0.0.1:8080/getUser 未登录,被拦截,跳转到/login(get模式),返回未登录提示
     2.通过postamn 以post模式访问: http://127.0.0.1:8080/login,参数通过body (x-wwww-from-urlencoded)表单提交
     2.1. 登录成功,进入ServerAuthenticationSuccessHandler类中加工返回数据
     2.2. 登录失败,进入ServerAuthenticationFailureHandler类中加工返回数据
     2.3. 登录成功后,继续通过postamn 以get访问:http://127.0.0.1:8080/getUser,响应正常
     */

    public static void main(String[] args) {
        SpringApplication.run(JwtAppStart.class, args);
    }

    /**
     * 不限制用户访问(需登录)
     * @return
     */
    @RequestMapping("/getUser")
    @ResponseBody
    public Mono<String> getUser(){
        return Mono.just("getUser");
    }

    /**
     * 普通用户访问
     * post
     * @return
     */
    @PreAuthorize("hasRole('USER')")
    @RequestMapping(value = "/user", method = {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public Mono<String> user(Principal principal){
        System.out.println(principal.getName());
        return Mono.just("hello "+principal.getName());
    }

    /**
     * 超管用户访问
     * @return
     */
//    @PreAuthorize("hasPermission('ROLE_ADMIN')")
//    @Secured("ROLE_ADMIN")
    @PreAuthorize("hasRole('ADMIN')")
    @RequestMapping(value = "/admin", method = {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public Mono<String> admin(Principal principal,Authentication authentication){
        System.out.println(authentication.getAuthorities());
        System.out.println(principal.getName());
        return Mono.just("admin "+principal.getName());
    }

    @RequestMapping(value = "/login", method = {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public Mono<Object> login(ServerHttpResponse response){
        Map<String, String> responseMap = new HashMap<>();
        responseMap.put("code", "failure");
        responseMap.put("msg", "您还未登录");
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return Mono.just(responseMap);
    }

}

创建权限配置类

WebFluxSecurityConfigurer.java

package com.webflux.demo.config;

import com.alibaba.fastjson.JSONObject;
import com.webflux.demo.handler.JwtSecurityContextRepository;
import com.webflux.demo.service.UserService;
import com.webflux.demo.util.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description EnableWebFluxSecurity权限验证配置
 * @Author JL
 * @Version V1.0
 */
@Slf4j
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class WebFluxSecurityConfigurer {

    /**
     * 提供用于获取UserDetails的Service
     * @param userService
     * @return
     */
    @Bean
    public ReactiveAuthenticationManager authenticationManager(UserService userService) {
        log.info("加载security 用户配置....");
        return new UserDetailsRepositoryReactiveAuthenticationManager(userService);
    }

    /**
     * http请求路径权限与过滤链配置
     * @param http
     * @param userService
     * @return
     */
    @Bean
    public SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http, UserService userService) {
        log.info("加载security 权限配置....");
        http
                .csrf().disable()
                .cors().disable()
                .httpBasic().disable()
                .securityContextRepository(new JwtSecurityContextRepository(userService))
                .formLogin()
                .authenticationFailureHandler((webFilterExchange, exception) -> { //验证失败处理器(可以单独创建类处理)
                    webFilterExchange.getExchange().getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                    Map<String, String> responseMap = new HashMap<>();
                    responseMap.put("code", "failure");
                    if (exception instanceof UsernameNotFoundException) {
                        responseMap.put("msg", "用户不存在 " + exception.getMessage());
                    } else if (exception instanceof BadCredentialsException) {
                        responseMap.put("msg", "密码错误 " + exception.getMessage());
                    } else if (exception instanceof LockedException) {
                        responseMap.put("msg", "用户锁定 " + exception.getMessage());
                    } else if (exception instanceof AccountExpiredException) {
                        responseMap.put("msg", "账户过期 " + exception.getMessage());
                    } else if (exception instanceof DisabledException) {
                        responseMap.put("msg", "账户不可用 " + exception.getMessage());
                    } else {
                        responseMap.put("msg", "系统错误 " + exception.getMessage());
                    }
                    //responseMap.put("msg", exception.getMessage());
                    return writeWith(webFilterExchange.getExchange(), responseMap);
                })
                .loginPage("/login")
                .authenticationSuccessHandler((webFilterExchange, authentication) -> { //验证成功处理器(可以单独创建类处理)
                    User user = (User)authentication.getPrincipal();
                    userService.saveCacheUser(user);
                    String token = JwtTokenUtils.createToken(user.getUsername(), -1);
                    Map<String, String> responseMap = new HashMap<>();
                    responseMap.put("code", "success");
                    responseMap.put("data", token);
                    return writeWith(webFilterExchange.getExchange(), responseMap);
                }).and()
                .authorizeExchange()//请求进行授权
                .pathMatchers(HttpMethod.OPTIONS).permitAll()//特殊请求过滤
                .pathMatchers("/login").permitAll()//登录不需要验证
                .anyExchange()//任何请求
                .authenticated()//都需要身份认证
                .and().logout()
                .logoutSuccessHandler((webFilterExchange, authentication) -> { //退出成功处理器(可以单独创建类处理)
                    Map<String, String> responseMap = new HashMap<>();
                    responseMap.put("code", "logout");
                    responseMap.put("msg", "退出成功");
                    return writeWith(webFilterExchange.getExchange(), responseMap);
                }).and()
                .exceptionHandling()
                .accessDeniedHandler((exchange, denied) -> { // 无权限访问处理器(可以单独创建类处理)
                    Map<String, String> responseMap = new HashMap<>();
                    responseMap.put("code", "denied");
                    responseMap.put("msg", "账户无权限访问");
                    return writeWith(exchange, responseMap);
                });

        return http.build();
    }

    /**
     * 输出响应信息
     * @param exchange
     * @param responseMap
     * @return
     */
    public Mono<Void> writeWith(ServerWebExchange exchange, Map<String, String> responseMap){
        ServerHttpResponse response = exchange.getResponse();
        String body = JSONObject.toJSONString(responseMap);
        DataBuffer buffer = null;
        try{
            buffer = response.bufferFactory().wrap(body.getBytes("UTF-8"));
        }catch(UnsupportedEncodingException ue){
            ue.printStackTrace();
        }
        return response.writeWith(Mono.just(buffer));
    }

}

创建上下文验证器

JwtSecurityContextRepository.java

package com.webflux.demo.handler;

import com.webflux.demo.service.UserService;
import com.webflux.demo.util.JwtTokenUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;

/**
 * @Description 获取请求头中带过来的token值,解析并验证用户信息
 * @Author JL
 * @Version V1.0
 */
@Slf4j
public class JwtSecurityContextRepository implements ServerSecurityContextRepository {

    private UserService userService;

    public JwtSecurityContextRepository(UserService userService){
        this.userService = userService;
    }

    @Override
    public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
        return Mono.empty();
    }

    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        log.info("加载token:JwtSecurityContextRepository");
        String path = exchange.getRequest().getPath().toString();
        // 过滤路径
        if ("/login".equals(path)){
            return Mono.empty();
        }
        String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (StringUtils.isNotBlank(token)){
            // token能正常解析,表示token有效并对应数据库已知用户
            String subject = JwtTokenUtils.parseToken(token);
            Authentication newAuthentication = new UsernamePasswordAuthenticationToken(subject, subject);
            return new ReactiveAuthenticationManager(){
                @Override
                public Mono<Authentication> authenticate(Authentication authentication) {
                    // 如果对token有足够的安全认可,可以采用无状态凭证策略,将username和authorities放置在token串中解析获取,此处就可以不用查询数据库验证
                    Mono<UserDetails> userDetails = userService.findByUsername(authentication.getPrincipal().toString());
                    UserDetails user = userDetails.block();
                    if (user == null){
                        throw new DisabledException("账户不可用");
                    }
                    Authentication auth = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
                    return Mono.just(auth);
                }
            }.authenticate(newAuthentication).map(SecurityContextImpl::new);
        }else {
            return Mono.empty();
        }
    }
}

创建用户身份验证业务类

UserService.java

package com.webflux.demo.service;

import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description 用户信息查询与登录状态保存
 * @Author JL
 * @Date 2021/08/12
 * @Version V1.0
 */
@Service
public class UserService implements ReactiveUserDetailsService {

    private static Map<String,UserDetails> userMap = new HashMap<>();

    /**
     * 获取指定用户信息与登录信息进行验证
     * @param username
     * @return
     */
    @Override
    public Mono<UserDetails> findByUsername(String username) {
        UserDetails userDetails = queryCacheUser(username);
        if (userDetails != null){
            return Mono.just(userDetails);
        }else {
            return Mono.just(queryDbUser(username));
        }
    }

    /**
     * 模拟查询redis缓存数据
     * @param username
     * @return
     */
    public UserDetails queryCacheUser(String username){
        return userMap.get(username);
    }

    /**
     * 模拟登录成功后保存redis缓存中
     * @param userDetails
     */
    public void saveCacheUser(UserDetails userDetails){
        userMap.put(userDetails.getUsername(), userDetails);
    }

    /**
     * 模拟查询数据库
     * @param username
     * @return
     */
    public UserDetails queryDbUser(String username){
        UserDetails userDetails = null;
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        if (username.equals("user")){
            userDetails = User.withUsername("user")
                    .password(encoder.encode("user"))
                    .roles("USER")
                    .build();
        }else if (username.equals("admin")) {
            userDetails = User.withUsername("admin")
                    .password(encoder.encode("admin"))
                    .roles("USER", "ADMIN")
                    .build();
        }else {
            throw new UsernameNotFoundException("无此账户");
        }
        System.out.println(userDetails.getPassword());
        return userDetails;
    }

}

创建JWT工具类

JwtTokenUtils.java

package com.webflux.demo.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import org.apache.commons.lang3.time.DateUtils;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description JWT跨域管理token工具类
 * @Author JL
 * @Version V1.0
 */
public class JwtTokenUtils {

    /**
     JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。
     JWT 的三个部分依次如下
     Header(头部):是一个 JSON 对象,描述 JWT 的元数据{ "alg": "HS256", typ": "JWT" }
     Payload(负载):也是一个 JSON 对象,用来存放实际需要传递的数据,JWT 规定了7个官方字段:
         iss (issuer):签发人
         exp (expiration time):过期时间
         sub (subject):主题
         aud (audience):受众
         nbf (Not Before):生效时间
         iat (Issued At):签发时间
         jti (JWT ID):编号
     Signature(签名):对前两部分的签名,防止数据篡改

     1.JWT中Header头和Payload有效载荷序列化的算法都用到了Base64URL,签名哈希部分是对Header与Payload两部分数据签名
     2.客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中,客户端将在与服务器交互中都会带JWT,将它放入HTTP请求的Header Authorization字段中
     3.JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限
     4.JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限
     5.JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输
     */

    /**
     * 加密密钥
     */
    private static final String SECRET = "f2f4f94c9065_wNmx01w27MQnPc3BtUQkty_23P0pVlAdj86o5XznUrE";

    /**
     * jwt创建token,考虑安全性,token中不因该放入太多信息(勿放密码之类的敏感信息),只放入关键字段值即可,如用户ID
     * @param sub     主题(可以放入关键数据,如:userid, 用户唯一值等)
     * @param timeout 过期时长(秒)
     * @return
     */
    public static String createToken(String sub, int timeout) {
        JWTCreator.Builder builder = JWT.create();
        builder.withSubject(sub);//主题
        builder.withIssuer("pro-server");
        if (timeout>0) {
            builder.withExpiresAt(DateUtils.addSeconds(new Date(), timeout));//过期时间
        }
        return builder.sign(Algorithm.HMAC256(SECRET));
    }

    /**
     * 对jwt创建的token进行验签与解析,返回Subject(主题)中存放的内容
     * @param token
     * @return
     * @throws TokenExpiredException          会话超时异常
     * @throws SignatureVerificationException 验签无效异常
     */
    public static String parseToken(String token) throws TokenExpiredException, SignatureVerificationException {
        return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token).getSubject();
    }

    /**
     * jwt创建token,考虑安全性,token中不因该放入太多信息(勿放密码之类的敏感信息)
     * @param loadMap   数据集合
     * @param timeout   过期时长(秒)
     * @return
     */
    public static String createToken(Map<String, Object> loadMap, int timeout) {
        JWTCreator.Builder builder = JWT.create();
        loadMap.forEach((k, v) -> {
            if (v instanceof String) {
                builder.withClaim(k, (String) v);
            } else if (v instanceof Date) {
                builder.withClaim(k, (Date) v);
            } else if (v instanceof Long) {
                builder.withClaim(k, (Long) v);
            } else if (v instanceof Integer) {
                builder.withClaim(k, (Integer) v);
            } else if (v instanceof Boolean) {
                builder.withClaim(k, (Boolean) v);
            }
        });
        builder.withIssuer("pro-server");
        if (timeout>0) {
            builder.withExpiresAt(DateUtils.addSeconds(new Date(), timeout));//过期时间
        }
        return builder.sign(Algorithm.HMAC256(SECRET));
    }

    /**
     * 对jwt创建的token进行验签与解析,返回集合
     * @param token
     * @return
     * @throws TokenExpiredException          会话超时异常
     * @throws SignatureVerificationException 验签无效异常
     */
    public static Map<String, Object> parseTokenToMap(String token) throws TokenExpiredException, SignatureVerificationException {
        Map<String, Claim> claimMap = JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token).getClaims();
        if (claimMap == null){
            return null;
        }
        Map<String, Object> loadMap = new HashMap<>();
        claimMap.forEach((k, v) -> {
            Object obj = null;
            if (v.asString() != null) {
                obj = v.asString();
            } else if (v.asBoolean() != null) {
                obj = v.asBoolean();
            } else if (v.asDate() != null || v.asLong() != null) {//Date类型按Long方式来处理
                obj = v.asLong();
            } else if (v.asInt() != null) {
                obj = v.asInt();
            }
            loadMap.put(k, obj);
        });
        return loadMap;
    }
}

项目测试

1.启动项目

运行JwtAppStart

2.测试未登录状态,访问/user接口

http://127.0.0.1:8080/user

{
    "msg": "您还未登录",
    "code": "failure"
}

3.测试权限为USER的普通用户登录接口

POST http://127.0.0.1:8080/login

username=user

password=user

// data返回为jwt生成的token值,后续请求需在Http的headers中添加Authorization=token值
{
    "code": "success",
    "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiaXNzIjoicHJvLXNlcnZlciJ9.9eTmhl5fD0iIajFgWVaWEijENUtE34OU4lVZCAenBvc"
}

注:通过post访问/login,属于SpringSecurity安全验证框架默认拦截内置登录路径;

4.测试登录后访问/user接口

Http请求headers中添加Authorization=token值

GET http://127.0.0.1:8080/user

hello user

5.测试登录后访问/admin接口

Http请求headers中添加Authorization=token值

GET http://127.0.0.1:8080/admin

//因控制器admin方法上有@PreAuthorize("hasRole('ADMIN')")注释,则需要验证用户权限为ADMIN才能访问
{
    "msg": "账户无权限访问",
    "code": "denied"
}

6.测试退出

Http请求headers中添加Authorization=token值

POST http://127.0.0.1:8080/logout

{
    "msg": "退出成功",
    "code": "logout"
}

注:通过post访问/logout,属于SpringSecurity安全验证框架默认拦截内置退出路径;

总结

本项目示例演示了在SpringBoot中用WebFlux非阻塞Web服务集成SpringSecurity安全验证框架,基于Netty容器上运行,并增加JWT数字签名,通过JWT识别用户唯一身份,通过SpringSecurity验证用户合法身份以及获取用户Role角色,通过@PreAuthorize注解标签,识别用户访问控制器方法是否具有权限角色;本示例过程只是基础演示,实际项目应用根据业务场景和要求对身份安全、权限管理会更加复杂;

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐