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

相關文章
相關標籤/搜索