Shrio uses Jwt to achieve front-end and back-end separation

Posted by greywire on Sat, 24 Aug 2019 12:45:48 +0200

Summary

After the front and back ends are separated, because HTTP itself is stateless, Session is useless. After the project adopts the scheme of jwt, the main process of request is as follows: after the user logs in successfully, the server will create a JWT token (the current operation account is recorded in the token of jwt), and return the token to the front end, which will put the token into the Header or Paramete every time the data of the server is requested by the front end. In r, when the server receives the request, it will be intercepted by the interceptor. The interceptor of token verification will get the token in the request, and then verify the validity of token. After the interceptors all verify the validity of token, the request will successfully arrive in the actual business process, and the execution of business logic will be returned to the front-end data. In this process, it mainly involves Shiro's interceptor chain, Jwt's token management, multi-Realm configuration and so on.

Shiro's Filter Chain

Shiro's authentication and authorization are inseparable from Filter, so it is necessary to have a clear operation process of Shiro's Filter in order to customize the Filter to meet the actual needs of enterprises. In addition, Shiro's Filter is similar in principle to Servlet's Filter, and even inherits the same interface eventually, but it's actually different. Filter in Shiro is mainly used in ShiroFilter to intercept matching URL s, which has its own filter chain; Servlet's Filter and ShiroFilter are of the same level, that is, Shiro's own filter system first, and then the Servlet container's FilterChain is entrusted with Servlet container-level filter chain holder. That's ok

Analysis of Shiro's default Filter

During the integration of Shiro and Spring Book, ShiroFilterFactoryBean needs to be configured, which is the ShiroFilter factory class and inherits the FactoryBean interface. It can be analyzed from the method of this interface. The interface getObject takes an instance and logically finds that the createFilterChainManager is called and creates the default Filter (guess Map < String, Filter > defaultFilters = manager. getFilters () by name).

public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
    private Map<String, Filter> filters;

    private Map<String, String> filterChainDefinitionMap; 

    /**
     *
     * Products produced in this factory
     */
    public Object getObject() throws Exception {
        if (instance == null) {
            instance = createInstance();
        }
        return instance;
    }

    protected FilterChainManager createFilterChainManager() {
        //Create default Filter
        DefaultFilterChainManager manager = new DefaultFilterChainManager();
        Map<String, Filter> defaultFilters = manager.getFilters();
        for (Filter filter : defaultFilters.values()) {
            applyGlobalPropertiesIfNecessary(filter);
        }

        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                manager.addFilter(name, filter, false);
            }
        }

        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                manager.createChain(url, chainDefinition);
            }
        }

        return manager;
    }

    protected AbstractShiroFilter createInstance() throws Exception {

        log.debug("Creating Shiro Filter instance.");

        SecurityManager securityManager = getSecurityManager();
        if (securityManager == null) {
            String msg = "SecurityManager property must be set.";
            throw new BeanInitializationException(msg);
        }

        if (!(securityManager instanceof WebSecurityManager)) {
            String msg = "The security manager does not implement the WebSecurityManager interface.";
            throw new BeanInitializationException(msg);
        }
        //Create FilterChain Manager
        FilterChainManager manager = createFilterChainManager();

        PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
        chainResolver.setFilterChainManager(manager);

        return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
    }
    
   ...
}

Add DefaultFilters in DefaultFilterChain Manager to add default Filters, which are a series of default Filter enumeration classes.

public class DefaultFilterChainManager implements FilterChainManager {
    
    public Map<String, Filter> getFilters() {
        return filters;
    }

    protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
        Filter existing = getFilter(name);
        if (existing == null || overwrite) {
            if (filter instanceof Nameable) {
                ((Nameable) filter).setName(name);
            }
            if (init) {
                initFilter(filter);
            }
            this.filters.put(name, filter);
        }
    }

     /**
     *
     * Create the default Filter
     */
    protected void addDefaultFilters(boolean init) {
        for (DefaultFilter defaultFilter : DefaultFilter.values()) {
            addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
        }
    }
    ...
}

From this enumeration class, you can see that there are 11 default filters added before, whose names are anon,authc,authcBaisc and so on.

public enum DefaultFilter {

    anon(AnonymousFilter.class),
    authc(FormAuthenticationFilter.class),
    authcBasic(BasicHttpAuthenticationFilter.class),
    logout(LogoutFilter.class),
    noSessionCreation(NoSessionCreationFilter.class),
    perms(PermissionsAuthorizationFilter.class),
    port(PortFilter.class),
    rest(HttpMethodPermissionFilter.class),
    roles(RolesAuthorizationFilter.class),
    ssl(SslFilter.class),
    user(UserFilter.class);

    private final Class<? extends Filter> filterClass;

    private DefaultFilter(Class<? extends Filter> filterClass) {
        this.filterClass = filterClass;
    }

    public Filter newInstance() {
        return (Filter) ClassUtils.newInstance(this.filterClass);
    }

    public Class<? extends Filter> getFilterClass() {
        return this.filterClass;
    }
    ...
}

Analysis of the Inheritance System of Filter

  • NameableFilter gives Filter a name. If not set, the default name is FilterName.

  • OncePerRequestFilter is used to prevent multiple executions of Filter; that is to say, one request only takes one interceptor chain; in addition, enabled attribute is provided to indicate whether to open the interceptor instance, enabled=true by default means open, if you do not want an interceptor to work, you can set it to false.

  • AdviceFilter provides AOP-style support. preHandler: Execute before the interceptor chain executes, continue the interceptor chain if it returns true; otherwise interrupt the execution of the subsequent interceptor chain and return directly; preprocess (such as authentication, authorization, etc.). PosHandle: Execute after the execution of the interceptor chain is completed, and post-process (such as recording execution time). After Completion: Similar to the post-final enhancement in AOP; that is, it executes regardless of exceptions and cleans up resources (such as contacting Subject and thread binding).

  • PathMatchingFilter has built-in examples of pathMatcher to facilitate the function of request path matching and interceptor parameter parsing. As shown below, the logic of isFilterChainContinued is executed for matched paths, and if not, it is directly handed over to the interceptor chain.

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {

    if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
        if (log.isTraceEnabled()) {
            log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
        }
        return true;
    }

    for (String path : this.appliedPaths.keySet()) {
        //Processing matching paths
        if (pathsMatch(path, request)) {
            log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
            Object config = this.appliedPaths.get(path);
            return isFilterChainContinued(request, response, path, config);
        }
    }

    return true;
}
  • Access Control Filter provides the basic function of access control. If isAccess Allowed access passes through, it is handed over to the interceptor chain. If not, onAccess Denied is executed to determine whether it is handed over to the interceptor or handled by itself.
 public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
        return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    }
  • Authentication Filter authenticates the base class of Filter, which generally performs authentication logic in isAccessAllowed. In addition, the Filter provides the function of jumping after successful login.
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object      mappedValue) {
    Subject subject = getSubject(request, response);
    return subject.isAuthenticated();
}


protected void issueSuccessRedirect(ServletRequest request, ServletResponse response) throws        Exception {
    WebUtils.redirectToSavedRequest(request, response, getSuccessUrl());
}
  • Authenticating Filter is a subclass of Authentication Filter, which provides executeLogin universal logic. It usually implements protected Abstract Authentication Token creation Token (Servlet Request request, Servlet Response response) method by subclasses, and then executes subject.login(token)
public abstract class AuthenticatingFilter extends AuthenticationFilter {
    public static final String PERMISSIVE = "permissive";

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        AuthenticationToken token = createToken(request, response);
        if (token == null) {
            String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " +
                "must be created in order to execute a login attempt.";
            throw new IllegalStateException(msg);
        }
        try {
            Subject subject = getSubject(request, response);
            subject.login(token);
            return onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException e) {
            return onLoginFailure(token, e, request, response);
        }
    }

    protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception;

    protected AuthenticationToken createToken(String username, String password,
                                              ServletRequest request, ServletResponse response) {
        boolean rememberMe = isRememberMe(request);
        String host = getHost(request);
        return createToken(username, password, rememberMe, host);
    }

    protected AuthenticationToken createToken(String username, String password,
                                              boolean rememberMe, String host) {
        return new UsernamePasswordToken(username, password, rememberMe, host);
    }

    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
                                     ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }

    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e,
                                     ServletRequest request, ServletResponse response) {
        return false;
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return super.isAccessAllowed(request, response, mappedValue) ||
            (!isLoginRequest(request, response) && isPermissive(mappedValue));
    }
    ...
}

Add custom Filter to Shiro

From the source analysis above, we know that Shiro will provide 11 default filters, which are also managed by the FilterChain Manager according to the interceptor mode and eventually returned to Spring Shiro Filter. So there are three main steps to add a custom Filter.

  • Implementing your own Filter

The following implements its own JwtFilter, the main logic can refer to the Form Authentication Filter. JwtFilter mainly checks the Api of the front end. If the test fails, it throws out abnormal information and does not process the interceptor chain.

@Slf4j
public class JwtFilter extends AuthenticatingFilter {   
    private static final String TOKEN_NAME = "token";
    
    /**
     * Create tokens
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        final String token = getToken((HttpServletRequest) servletRequest);     
        if(StringUtils.isEmpty(token)) {
            return null;
        }       
        return new JwtToken(token);     
    }
    
    /**
     * Getting tokens
     * @param httpServletRequest
     * @return
     */
    private String getToken(HttpServletRequest httpServletRequest) {
        String token = httpServletRequest.getHeader(TOKEN_NAME);
        if(StringUtils.isEmpty(token)) {
            token = httpServletRequest.getParameter(TOKEN_NAME);
        };
        if(StringUtils.isEmpty(token)) {
            Cookie[] cookies = httpServletRequest.getCookies();
            if(ArrayUtils.isNotEmpty(cookies)) {
                for(Cookie cookie :cookies) {
                    if(TOKEN_NAME.equals(cookie.getName())) {
                        token = cookie.getValue();
                        break;
                    }
                }
            }
        };  
        return token;
    }
 
    /**
     * Unhandled
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        return executeLogin(servletRequest, servletResponse);
    }

    /**
     * Logon Failure Execution Method
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request,
            ServletResponse response) {
        response.setContentType("text/html;charset=UTF-8");
        try(OutputStream outputStream = response.getOutputStream()){
            outputStream.write(e.getMessage().getBytes(SystemConsts.CHARSET));
            outputStream.flush();           
        } catch (IOException e1) {
            e1.printStackTrace();
        }   
        return false;
    }
    ...
}
  • Add Filter to Shiro

Add a custom Filter to Shiro and specify the matching path.

public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Autowired          org.apache.shiro.mgt.SecurityManager securityManager, @Autowired JwtFilter jwtFilter) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        Map<String, Filter> filterMap = new LinkedHashMap<>();
        filterMap.put("jwt", jwtFilter);
        shiroFilterFactoryBean.setFilters(filterMap);
    
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/**", "jwt"); 
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    ...
        return shiroFilterFactoryBean;
    }

Note: SpringBook automatically registers our Filter(Filter is registered to the entire Filter chain, not Shiro's Filter chain), but in Shiro, we need to register ourselves, but we also need Filter instances to exist in the Spring container so that we can use many other services (automatic injection of other components... ) So you need to cancel Spring Boot's automatic injection of Filter. The following can be used:

@Bean
public FilterRegistrationBean registration(@Qualifier("devCryptoFilter") DevCryptoFilter filter){
    FilterRegistrationBean registration = new FilterRegistrationBean(filter);
    registration.setEnabled(false);
    return registration;
}

Jwt Integration

Using Jwt requires us to provide a way to create, verify and obtain information in token. There are many things on the Internet that can be used for reference, and some other data can be stored in token.

public class JwtUtil {

    /**
     * Test token
     * @return boolean
     */
    public static boolean verify(String token, String username) {
        ...
    }

    /**
     * Get attributes in token
     * @return token Attributes included in
     */
    public static String getValue(String token, String key) {
        ...
    }

    /**
     * The token signature EXPIRE_TIME expires in minutes
     * 
     * @param username
     *            User name
     * @return Encrypted token
     */
    public static String createJWT(String userId) {
        ...
    }
}

Multiple Realm Configuration

User password authentication and Jwt authentication need two different Realms. Multiple Realms need to deal with different Realms to obtain the data model of Authentication Token of the specified Realm.

  • Method of Implementing Modular Realm Authenticator
public class MultiRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) 
            throws AuthenticationException {
        assertRealmsConfigured();
        
        List<Realm> realms = this.getRealms()
                .stream()
                .filter(realm -> {
                    return realm.supports(authenticationToken);
                })
                .collect(Collectors.toList());
        
        return realms.size() == 1 ? this.doSingleRealmAuthentication(realms.get(0), authenticationToken) : 
            this.doMultiRealmAuthentication(realms, authenticationToken);
    }
}
  • Implementation of getAuthentication TokenClass Method in Authenticating Realm
public Class getAuthenticationTokenClass() {
    return JwtToken.class;
}
  • Configuration in Security Manager
@Bean(name = "securityManager")
public org.apache.shiro.mgt.SecurityManager defaultWebSecurityManager(@Autowired UserRealm      userRealm,  @Autowired TokenRealm tokenValidateRealm) {
    securityManager.setAuthenticator(multiRealmAuthenticator());
    securityManager.setRealms(Arrays.asList(userRealm, tokenValidateRealm));
    ...
    return securityManager;
}

Integrating Swagger

Adding Swagger dependencies

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

Add Swagger configuration

@Configuration
public class Swagger2Config {

    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("XXX"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("XXX")
                .description("For reference")
                .version("1.0")
                .build();
    }
}

summary

Throughout the process, the pit encountered is the automatic injection of Filter in Spring boot. In the middle of the pit, the solution without injection is considered. That is to say, the new JwtFilter() can be used directly. Although it can solve the problem, it is not perfect, and finally the solution is found on the Internet. The implementation process of Shiro's Filter Chain has been enhanced, and self-defined Filter can be used to solve practical problems. There is also a follow-up problem, the token processing of Jwt when it logs out, which itself can not be cleared as Session, as long as it does not expire in theory, it will always exist. Consider using caches, which can be cleared when you exit, and then retrieved from the cache for judgment when checking.

Topics: Java Shiro Spring Session Apache