Spring security - integrate JWT to use Token authentication and authorization in Webflux environment

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.

Keywords: Java Back-end Microservices

Added by jameslynns on Wed, 19 Jan 2022 12:59:37 +0200