Spring WebFlux (7): Springboot Security+jwt登陆鉴权

Spring WebFlux (3): mysql+Springboot Security实现登陆鉴权的基础上实现css

token登陆的逻辑刚上手确实很复杂,挺难啃的,并且实现方法也不惟一,看过不少博客实现的方法基本都不同,简单说一下个人方法:html

  • 首先设置一个WebFilter,主要两个功能:java

    1. 登陆和注册时是没有token的,这两个功能的路由放行
    2. 其余请求检查token,是否有token,token是否合法,讲处理的token放入上下文中
  • 而后就是实现ServerSecurityContextRepositorymysql

  • 实现ReactiveAuthenticationManager中的authenticate 方法 解析token,将解析的权限信息写入Authentication对象react

  • 经过ServerSecurityContextRepository类中的load方法获取到上下文中的token,将token装到Authentication对象中,再经过ReactiveAuthenticationManager中的authenticate 方法获取到有权限信息的Authentication对象传入Security上下文中git

  • 经过以上几个步骤实现登陆和鉴权github

项目依赖

webflux + mysql + r2dbc + jwtweb

openapi还须要配置一下添加token的功能,使用须要设置headerspring

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation 'org.springdoc:springdoc-openapi-webflux-ui:1.4.1'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'dev.miku:r2dbc-mysql'
    runtimeOnly 'mysql:mysql-connector-java'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'io.projectreactor:reactor-test'
    testImplementation 'org.springframework.security:spring-security-test'
}

1. 添加登陆相关功能

添加两个接口:sql

  • /auth/login: 实现登陆功能,输入username和password返回token权限等信息
  • /auth/signup: 实现注册功能
/** * @author: ffzs * @Date: 2020/8/16 下午8:52 */

@RestController
@RequiredArgsConstructor
@RequestMapping("auth")
@Slf4j
public class LoginController {

    private final MyUserDetailsRepository myUserRepository;
    private final MyUserService myUserService;
    private final JwtSigner jwtSigner;
    private final PasswordEncoder password = PasswordEncoderFactories.createDelegatingPasswordEncoder();

    @PostMapping("login")
    public Mono<HttpResult> login (@RequestBody Map<String, String> user) {

        return Mono.just(user.get("username"))
                .flatMap(myUserRepository::findByUsername)
                .filter(it -> password.matches(user.get("password"), it.getPassword()))
                .map(it -> new HttpResult(HttpStatus.OK.value(), "成功登陆", new LoginResponse(it.getUsername(), it.getAuthorities().toString(), jwtSigner.generateToken(it))))
                .onErrorResume(e -> Mono.empty())
                .switchIfEmpty(Mono.just(new HttpResult(HttpStatus.UNAUTHORIZED.value(), "登陆失败", null)));
    }


    @PostMapping("signup")
    public Mono<HttpResult> signUp (@RequestBody MyUser user) {

        return Mono.just(user)
                .map(myUserService::save)
                .map(it -> new HttpResult(HttpStatus.OK.value(), "注册成功", null))
                .onErrorResume(e -> Mono.just(new HttpResult(HttpStatus.UNAUTHORIZED.value(), "注册失败", e)));
    }
}
  • 由于登陆在controller内部完成,而且须要根据相关信息生成jwt token,须要编写一个用来生成token以及拆解token的类

2. 编写jwt功能服务类

该类有几个功能:

  • 根据UserDetails中帐号名,权限等信息生成token
  • 可以将token还原成原来传入的信息用于获取权限信息
  • 提供一些jwt相关参数
  • 关联配置文件中配置token失效时间

完整代码以下:

/** * @author: ffzs * @Date: 2020/8/16 下午8:06 */

@Service
@RequiredArgsConstructor
@Slf4j
public class JwtSigner {

    private final MyUserDetailsRepository myUserRepository;

    private final String key = "justAJwtSingleKey";
    private final String authorities = "authorities";
    private final String issuer = "identity";
    private final String TOKEN_PREFIX = "Bearer ";

    @Value("${jwt.expiration.duration}")
    private int duration ;


    public String getAuthoritiesTag () {
        return authorities;
    }

    public String getIssuerTag () {
        return issuer;
    }

    public String getTokenPrefix () {
        return TOKEN_PREFIX;
    }

    public String generateToken (String username) {

        return generateToken(Objects.requireNonNull(myUserRepository.findByUsername(username).block()));
    }

    public String generateToken (MyUserDetails user) {

        return Jwts.builder()
                .setSubject(user.getUsername())
                .claim(authorities, user.getAuthorities())
                .signWith(SignatureAlgorithm.HS256, key)
                .setIssuer(issuer)
                .setExpiration(Date.from(Instant.now().plus(Duration.ofMinutes(duration))))
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .compact();
    }

    public Claims parseToken (String token) {
        log.info("token : {}", token);
        return Jwts
                .parser()
                .setSigningKey(key)
                .parseClaimsJws(token)
                .getBody();
    }
}
  • 实现过滤信息的Filter

3. 过滤token的Filter实现

前面也提到该Filter有两个功能:

  • 过滤掉处登陆注册以外全部没有token的访问
  • 过滤掉token不合法
  • 更具不一样类型问题返回对应的报错信息
/** * @author: ffzs * @Date: 2020/8/17 下午12:53 */

@Component
@Slf4j
@AllArgsConstructor
public class JwtWebFilter implements WebFilter {

    private final JwtSigner jwtSigner;

    protected Mono<Void> writeErrorMessage(ServerHttpResponse response, HttpStatus status, String msg) throws JsonProcessingException, UnsupportedEncodingException {
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        ObjectMapper mapper=new ObjectMapper();
        String body = mapper.writeValueAsString(new HttpResult(status.value(), msg, null));
        DataBuffer dataBuffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Mono.just(dataBuffer));
    }

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();

        String path = request.getPath().value();
        if (path.contains("/auth/login") || path.contains("/auth/signout")) return chain.filter(exchange);

        String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (auth == null) {
            return this.writeErrorMessage(response, HttpStatus.NOT_ACCEPTABLE, "没有携带token");
        }
        else if (!auth.startsWith(jwtSigner.getTokenPrefix())) {
            return this.writeErrorMessage(response, HttpStatus.NOT_ACCEPTABLE, "token 没有以" + jwtSigner.getTokenPrefix() + "开始");
        }

        String token = auth.replace(jwtSigner.getTokenPrefix(),"");
        try {
            exchange.getAttributes().put("token", token);
        } catch (Exception e) {
            return this.writeErrorMessage(response, HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
        }

        return chain.filter(exchange);
    }
}

以后就是经过ServerSecurityContextRepository类将token内容写入SecurityContext

4. ServerSecurityContextRepository功能实现

  • 获取上下文中的token
  • 将token传入AuthenticationManagerauthenticate方法
  • authenticate方法解析以后完成的Authentication写入SecurityContext
/** * @author: ffzs * @Date: 2020/8/16 下午8:05 */

@Component
@AllArgsConstructor
@Slf4j
public class SecurityContextRepository implements ServerSecurityContextRepository {

    private final JwtAuthenticationManager jwtAuthenticationManager;

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

    @SneakyThrows
    @Override
    public Mono<SecurityContext> load(ServerWebExchange exchange) {
        log.info("访问 ServerSecurityContextRepository 。。。。。。。。。。。");

        String token = exchange.getAttribute("token");
        return jwtAuthenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(token, token)
            )
            .map(SecurityContextImpl::new);
    }
}

5. authenticate方法实现

  • 上一步传入的token进行解析
  • 解析的信息写入Authentication
/** * @author: ffzs * @Date: 2020/8/16 下午6:18 */

@Component
@AllArgsConstructor
@Slf4j
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {

    private final JwtSigner jwtSigner;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        log.info("访问 ReactiveAuthenticationManager 。。。。。。。。。。。");
        return Mono.just(authentication)
                .map(auth -> jwtSigner.parseToken(auth.getCredentials().toString()))
                .log()
                .onErrorResume(e -> {
                    log.error("验证token时发生错误,错误类型为: {},错误信息为: {}", e.getClass(), e.getMessage());
                    return Mono.empty();
                })
                .map(claims -> new UsernamePasswordAuthenticationToken(
                        claims.getSubject(),
                        null,
                        Stream.of(claims.get(jwtSigner.getAuthoritiesTag()))
                                .peek(info -> log.info("auth权限信息 {}", info))
                                .map(it -> (List<Map<String, String>>)it)
                                .flatMap(it -> it.stream()
                                        .map(i -> i.get("authority"))
                                        .map(SimpleGrantedAuthority::new))
                                .collect(Collectors.toList())
                ));
    }
}

6. Security配置

  • /auth/login/auth/signup的放行
  • 对filter的设置
  • jwtWebFilter执行必定要在SecurityContextRepository以前,否则的话上下文中没有token
/** * @author: ffzs * @Date: 2020/8/11 下午4:22 */

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
@AllArgsConstructor
public class SecurityConfig {

    private final SecurityContextRepository securityRepository;

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(
                ServerHttpSecurity http,
                JwtWebFilter jwtWebFilter
        ) {

        return http
                .authorizeExchange()
                .pathMatchers("/auth/login", "/auth/signup").permitAll()
                .pathMatchers("/v3/api-docs/**", "/swagger-resources/configuration/ui",
                        "/swagger-resources","/swagger-resources/configuration/security",
                        "/swagger-ui.html","/css/**", "/js/**","/images/**", "/webjars/**", "**/favicon.ico", "/index").permitAll()
                .anyExchange().authenticated()
                .and()
                .addFilterAfter(jwtWebFilter, SecurityWebFiltersOrder.FIRST)  // 这里注意执行位置必定要在securityContextRepository
                .securityContextRepository(securityRepository)
                .formLogin().disable()
                .httpBasic().disable()
                .csrf().disable()
                .logout().disable()
                .build();
    }
}

7. application.yml

  • 设置mysql连接
  • 设置token的持续时间(分钟)
spring:
  r2dbc:
    username: root
    password: 123zxc
    url: r2dbcs:mysql://localhost:3306/mydb?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8

jwt:
  expiration:
    duration: 3600

8. 测试

  • 登陆

在这里插入图片描述

  • 访问

在这里插入图片描述

代码

github
gitee

相关文章
相关标签/搜索