1, SpringSecurity - WebFlux
In the previous article, we explained the dynamic role permission control of spring security in WebFlux environment. In this article, we will explain how spring security integrates JWT and uses Token authentication and authorization in WebFlux environment.
Last article address: https://blog.csdn.net/qq_43692950/article/details/122511037
2, Integrate JWT to use Token authentication authorization
The use of spring security in WebFlux environment has been explained in previous articles. Before reading this article, you'd better have read the previous articles on spring security in this column, and some repetitive code will not be written. Let's go directly to the topic.
Before we start, let's be clear about one problem. Spring security has already implemented login authentication for us. We can specify the login path. The default is x-www-form-urlencoded. Therefore, we do not need to write login logic, but in some cases, the provided by spring security may not meet our needs. For example, when we are user-defined encrypted data transmission, we can write a login interface ourselves, issue Token tokens in this interface, and set formlogin() of ServerHttpSecurity object Disable(), the following demonstration uses the authentication provided by spring security.
Writing JWT tool classes
Here, I also put permissions in JWT. If you need to dynamically change user permissions, you can consider putting them in Redis or other NoSql databases. This article mainly demonstrates the use of JWT:
@Data @Component public class JwtTool { private String key = "com.bxc"; private long overtime = 1000 * 60 * 60; public String CreateToken(String userid, String username, List<String> roles) { long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); JwtBuilder builder = Jwts.builder() .setId(userid) .setSubject(username) .setIssuedAt(now) .signWith(SignatureAlgorithm.HS256, key) .claim("roles", roles); if (overtime > 0) { builder.setExpiration(new Date(nowMillis + overtime)); } return builder.compact(); } public boolean VerityToken(String token) { try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); if (claims != null) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; } public String getUserid(String token) { try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); if (claims != null) { return claims.getId(); } } catch (Exception e) { e.printStackTrace(); } return null; } public String getUserName(String token) { try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); if (claims != null) { return claims.getSubject(); } } catch (Exception e) { e.printStackTrace(); } return null; } public List<String> getUserRoles(String token) { try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); if (claims != null) { return (List<String>) claims.get("roles"); } } catch (Exception e) { e.printStackTrace(); } return null; } public String getClaims(String token, String param) { try { Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(token) .getBody(); if (claims != null) { return claims.get(param).toString(); } } catch (Exception e) { e.printStackTrace(); } return null; } }
Write a successful login Handler
Here we can do the logic of Token token:
@Component public class LoginSuccessHandler implements ServerAuthenticationSuccessHandler { @Autowired JwtTool jwtTool; @Override public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) { UserEntity user = (UserEntity) authentication.getPrincipal(); String username = user.getUsername(); List<GrantedAuthority> authorities = (List<GrantedAuthority>) user.getAuthorities(); List<String> roles = authorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); String token = jwtTool.CreateToken(String.valueOf(user.getId()), username, roles); JSONObject params = new JSONObject(); params.put("code", 200); params.put("msg", "Login succeeded!"); params.put("username", username); params.put("role", roles); params.put("token", token); ServerWebExchange exchange = webFilterExchange.getExchange(); ServerHttpResponse response = exchange.getResponse(); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); Mono<Void> ret = null; try { ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8"))))); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return ret; } }
Write a Handler for login failure
A friendly prompt is returned to the client. Here I directly return the login failure. You can make specific judgment according to the AuthenticationException class and return specific error information:
@Component public class LoginFailedHandler implements ServerAuthenticationFailureHandler { @Override public Mono<Void> onAuthenticationFailure(WebFilterExchange webFilterExchange, AuthenticationException e) { JSONObject params = new JSONObject(); params.put("code", 400); params.put("msg", "Login failed!"); ServerHttpResponse response = webFilterExchange.getExchange().getResponse(); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); Mono<Void> ret = null; try { ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8"))))); } catch (UnsupportedEncodingException e0) { e0.printStackTrace(); } return ret; } }
Write filters for JWT
Since the JWT Token has been issued above, the first step of the request is to filter and verify the JWT. If OK, it will be handed over to spring security to parse the JWT content, so this filter is only used to teach and research whether the JWT is effective or not, and does not authorize the current request:
@Slf4j @Component public class JwtWebFilter implements WebFilter { @Autowired JwtTool jwtTool; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); HttpHeaders header = response.getHeaders(); header.add("Content-Type", "application/json; charset=UTF-8"); String path = request.getPath().value(); if (path.contains("/auth/login")){ return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("token"); if (StringUtils.isBlank(token)) { JSONObject jsonObject = setResultErrorMsg(401,"Login failure"); DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes()); return response.writeWith(Mono.just(buffer)); } boolean isold = jwtTool.VerityToken(token); if (!isold) { JSONObject jsonObject = setResultErrorMsg(401,"Login failure"); DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes()); return response.writeWith(Mono.just(buffer)); } String username = jwtTool.getUserName(token); if (com.alibaba.druid.util.StringUtils.isEmpty(username)) { JSONObject jsonObject = setResultErrorMsg(401,"Login failure"); DataBuffer buffer = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes()); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } private JSONObject setResultErrorMsg(Integer code,String msg) { JSONObject jsonObject = new JSONObject(); jsonObject.put("code", code); jsonObject.put("message", msg); return jsonObject; } }
Parse user information in JWT and grant role permission information
The above is just a preliminary filtering of JWT. At this point, we need to analyze the information in JWT and set up a UsernamePasswordAuthenticationToken for user authorization. Here, I do the JWT verification again. In fact, JWT verification can not be done here. The previous filter has been verified, and the content can be taken directly,
@Slf4j @Component public class JwtSecurityContextRepository implements ServerSecurityContextRepository { @Autowired JwtTool jwtTool; @Override public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) { return Mono.empty(); } @Override public Mono<SecurityContext> load(ServerWebExchange exchange) { String path = exchange.getRequest().getPath().toString(); // Filter path if ("/auth/login".equals(path)) { return Mono.empty(); } String token = exchange.getRequest().getHeaders().getFirst("token"); if (StringUtils.isBlank(token)) { throw new DisabledException("Login failed!"); } boolean isold = jwtTool.VerityToken(token); if (!isold) { throw new AccessDeniedException("Login failed!"); } String username = jwtTool.getUserName(token); if (com.alibaba.druid.util.StringUtils.isEmpty(username)) { throw new AccessDeniedException("Login failed!"); } Authentication newAuthentication = new UsernamePasswordAuthenticationToken(username, username); return new ReactiveAuthenticationManager() { @Override public Mono<Authentication> authenticate(Authentication authentication) { return Mono.fromCallable(() -> { List<String> roles = jwtTool.getUserRoles(token); List<GrantedAuthority> authorities = roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); UserEntity principal = new UserEntity(); principal.setUsername(username); return new UsernamePasswordAuthenticationToken(principal, null, authorities); }); } }.authenticate(newAuthentication).map(SecurityContextImpl::new); } }
Determine whether the user has access to the interface
The above only obtains the role permission information owned by the user. The following also determines whether the role user required to access the interface owns it. The logic of this place is explained in the previous article. You can refer to the previous article:
@Component public class AuthManagerHandler implements ReactiveAuthorizationManager<AuthorizationContext> { @Autowired MeunMapper meunMapper; @Autowired RoleMapper roleMapper; private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) { ServerHttpRequest request = object.getExchange().getRequest(); String requestUrl = request.getPath().pathWithinApplication().value(); List<MeunEntity> list = meunMapper.selectList(null); List<String> roles = new ArrayList<>(); list.forEach(m -> { if (antPathMatcher.match(m.getPattern(), requestUrl)) { List<String> allRoleByMenuId = roleMapper.getAllRoleByMenuId(m.getId()) .stream() .map(r -> r.getRole()) .collect(Collectors.toList()); roles.addAll(allRoleByMenuId); } }); if (roles.isEmpty()) { return Mono.just(new AuthorizationDecision(false)); } return authentication .filter(a -> a.isAuthenticated()) .flatMapIterable(a -> a.getAuthorities()) .map(g -> g.getAuthority()) .any(c -> { if (roles.contains(String.valueOf(c))) { return true; } return false; }) .map(hasAuthority -> new AuthorizationDecision(hasAuthority)) .defaultIfEmpty(new AuthorizationDecision(false)); } @Override public Mono<Void> verify(Mono<Authentication> authentication, AuthorizationContext object) { return null; } }
Write a prompt Handler that does not have access
@Component public class AccessDeniedHandler implements ServerAccessDeniedHandler { @Override public Mono<Void> handle(ServerWebExchange serverWebExchange, AccessDeniedException e) { JSONObject params = new JSONObject(); params.put("code", 403); params.put("msg", "Insufficient permissions!"); ServerHttpResponse response = serverWebExchange.getResponse(); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); Mono<Void> ret = null; try { ret = response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(params.toJSONString().getBytes("UTF-8"))))); } catch (UnsupportedEncodingException e0) { e0.printStackTrace(); } return ret; } }
Modify SecurityConfig configuration
Configure the above into spring security:
@Configuration @EnableWebFluxSecurity @EnableReactiveMethodSecurity public class SecurityConfig { @Autowired UserDetailService userDetailService; @Autowired AuthManagerHandler authManagerHandler; @Autowired AccessDeniedHandler accessDeniedHandler; @Autowired LoginSuccessHandler loginSuccessHandler; @Autowired LoginFailedHandler loginFailedHandler; @Autowired LoginLoseHandler loginLoseHandler; @Autowired JwtSecurityContextRepository jwtSecurityContextRepository; @Autowired JwtWebFilter jwtWebFilter; //Authentication exclusion list of security private static final String[] excludedAuthPages = { "/auth/login", "/auth/logout" }; @Bean public ReactiveAuthenticationManager authenticationManager() { UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = new UserDetailsRepositoryReactiveAuthenticationManager(userDetailService); authenticationManager.setPasswordEncoder(passwordEncoder()); return authenticationManager; } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception { http.authorizeExchange() .pathMatchers(excludedAuthPages).permitAll() //Request path without permission filtering .pathMatchers(HttpMethod.OPTIONS).permitAll() //o .pathMatchers("/**").access(authManagerHandler) .anyExchange().authenticated() .and() .addFilterAfter(jwtWebFilter, SecurityWebFiltersOrder.FIRST) .securityContextRepository(jwtSecurityContextRepository) .formLogin() .loginPage("/auth/login") .authenticationSuccessHandler(loginSuccessHandler) .authenticationFailureHandler(loginFailedHandler) .and().exceptionHandling().authenticationEntryPoint(loginLoseHandler) .and().exceptionHandling().accessDeniedHandler(accessDeniedHandler) .and().cors().disable().csrf().disable(); return http.build(); } }
3, Effect demonstration
No login, direct access http://localhost:8080/admin/test , you will be prompted that the login is invalid.
Sign in http://localhost:8080/auth/login , get the returned Token:
Next, use the returned Token to test the above interface again:
If you access an unauthorized interface: http://localhost:8080/common/test
Love little buddy can pay attention to my personal WeChat official account and get more learning materials.