在Spring WebFlux (3): mysql+Springboot Security實現登陸鑑權的基礎上實現css
token登陸的邏輯剛上手確實很複雜,挺難啃的,並且實現方法也不惟一,看過不少博客實現的方法基本都不同,簡單說一下個人方法:html
-
首先設置一個WebFilter,主要兩個功能:java
- 登陸和註冊時是沒有token的,這兩個功能的路由放行
- 其餘請求檢查token,是否有token,token是否合法,講處理的token放入上下文中
-
而後就是實現
ServerSecurityContextRepository
類mysql -
實現
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傳入
AuthenticationManager
的authenticate
方法 - 將
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. 測試
- 登陸
- 訪問