zoukankan      html  css  js  c++  java
  • Shrio使用Jwt达到前后端分离

    概述

    前后端分离之后,因为HTTP本身是无状态的,Session就没法用了。项目采用jwt的方案后,请求的主要流程如下:用户登录成功之后,服务端会创建一个jwt的token(jwt的这个token中记录了当前的操作账号),并将这个token返回给前端,前端每次请求服务端的数据时,都会将令牌放入Header或者Parameter中,服务端接收到请求后,会先被拦截器拦截,token检验的拦截器会获取请求中的token,然后会检验token的有效性,拦截器都检验成功后,请求会成功到达实际的业务流程中,执行业务逻辑返回给前端数据。在这个过程中,主要涉及到Shiro的拦截器链,Jwt的token管理,多Realm配置等。

    Shiro的Filter链

    Shiro的认证和授权都离不开Filter,因此需要对Shiro的Filter的运行流程很清楚,才能自定义Filter来满足企业的实际需要。另外Shiro的Filter虽然原理都和Servlet的Filter相似,甚至都最终继承相同的接口,但是实际还是有些差别。Shiro中的Filter主要是在ShiroFilter内,对指定匹配的URL进行拦截处理,它有自己的Filter链;而Servlet的Filter和ShiroFilter是同一个级别的,即先走Shiro自己的Filter体系,然后才会委托给Servlet容器的FilterChain进行Servlet容器级别的Filter链执行

    分析Shiro的默认Filter

    在Shiro和Spring Boot整合过程中,需要配置ShiroFilterFactoryBean,该类是ShiroFilter的工厂类,并继承了FactoryBean接口。可以从该接口的方法来分析。该接口getObject获取一个实例,按照逻辑,发现调用createFilterChainManager,并创建默认的Filter(按照命名猜测Map<String, Filter> defaultFilters = manager.getFilters())。

    public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
        private Map<String, Filter> filters;
    
        private Map<String, String> filterChainDefinitionMap; 
    
        /**
         *
         * 该工厂类生产的产品类
         */
        public Object getObject() throws Exception {
            if (instance == null) {
                instance = createInstance();
            }
            return instance;
        }
    
        protected FilterChainManager createFilterChainManager() {
    		//创建默认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);
            }
    		//创建FilterChainManager
            FilterChainManager manager = createFilterChainManager();
    
            PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
            chainResolver.setFilterChainManager(manager);
    
            return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
        }
        
       ...
    }
    
    

    DefaultFilterChainManageraddDefaultFilters来添加默认的Filter,DefaultFilter为一系列默认Filter的枚举类。

    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);
            }
        }
    
         /**
         *
         * 创建默认的Filter
         */
        protected void addDefaultFilters(boolean init) {
            for (DefaultFilter defaultFilter : DefaultFilter.values()) {
                addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
            }
        }
        ...
    }
    
    

    从这个枚举类中可以看到之前添加的共有11个默认Filter,它们的名字分别是anon,authc,authcBaisc等。

    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;
        }
    	...
    }
    

    Filter的继承体系分析

    • NameableFilter给Filter起个名字,如果没有设置,默认名字就是FilterName。

    • OncePerRequestFilter用于防止多次执行Filter;也就是说一次请求只会走一次拦截器链;另外提供 enabled 属性,表示是否开启该拦截器实例,默认 enabled=true 表示开启,如果不想让某个拦截器工作,可以设置为 false 即可。

    • AdviceFilter提供了AOP风格的支持。preHandler:在拦截器链执行之前执行,如果返回true则继续拦截器链;否则中断后续的拦截器链的执行直接返回;可以进行预处理(如身份验证、授权等行为)。postHandle:在拦截器链执行完成后执行,后置处理(如记录执行时间之类的)。afterCompletion:类似于AOP中的后置最终增强;即不管有没有异常都会执行,可以进行清理资源(如接触 Subject 与线程的绑定之类的)。

    • PathMatchingFilter内置了pathMatcher的实例,方便对请求路径匹配功能及拦截器参数解析的功能,如下所示,对匹配的路径执行isFilterChainContinued的逻辑,如果都没配到,则直接交给拦截器链。

    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()) {
            //对匹配路径进行处理
            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;
    }
    
    • AccessControlFilter提供了访问控制的基础功能,isAccessAllowed访问通过,则交给拦截器链,不通过则执行onAccessDenied来确定交给拦截器还是自己处理
     public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
        }
    
    
    • AuthenticationFilter认证Filter的基类,一般在isAccessAllowed中执行认证逻辑,另外该Filter提供登录成功后跳转的功能
    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());
    }
    
    • AuthenticatingFilter是AuthenticationFilter的子类,提供了executeLogin通用逻辑,通常由子类来实现protected abstract AuthenticationToken createToken(ServletRequest request, ServletResponse response)该方法,然后执行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));
        }
        ...
    }
    

    在Shiro中添加自定义的Filter

    从上面源码分析,知道了Shiro会提供11个默认的Filter,也是按照拦截器模式交由FilterChainManager来管理Filter,并最终返回SpringShiroFilter。所以添加自定义的Filter,主要有三步。

    • 实现自己的Filter

    如下实现了自己的JwtFilter,主要逻辑可以参考FormAuthenticationFilter。JwtFilter主要是对前端的Api进行校验,检验失败,则抛出异常信息,不给拦截器链处理。

    @Slf4j
    public class JwtFilter extends AuthenticatingFilter {	
    	private static final String TOKEN_NAME = "token";
    	
    	/**
    	 * 创建令牌
    	 */
        @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);   	
        }
        
        /**
         * 获取令牌
         * @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;
        }
     
        /**
         * 未通过处理
         * @param servletRequest
         * @param servletResponse
         * @return
         * @throws Exception
         */
        @Override
        protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        	return executeLogin(servletRequest, servletResponse);
        }
    
        /**
         * 登录失败执行方法
         * @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;
    	}
        ...
    }
    
    • 将Filter添加到Shiro中

    将自定义的Filter添加到Shiro,并要指定的匹配路径。

    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;
    	}
    

    注意:SpringBoot自动帮我们注册了我们的Filter(Filter是注册到整个Filter链,而不是Shiro的Filter链),但是在Shiro中,我们需要自己实现注册,但是又需要Filter实例存在于Spring容器中,以便能使用其他众多服务(自动注入其他组件……)。所以需要取消Spring Boot的自动注入Filter。可以采用如下方式:

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

    Jwt整合

    使用Jwt需要我们提供对token的创建,校验和获取token中信息的方法。网上有很多,可以借鉴,而且token中也可以存一些其他数据。

    public class JwtUtil {
    
        /**
    	 * 检验token
    	 * @return boolean
    	 */
    	public static boolean verify(String token, String username) {
    		...
    	}
    
    	/**
    	 * 获得token中的属性
    	 * @return token中包含的属性
    	 */
    	public static String getValue(String token, String key) {
    		...
    	}
    
    	/**
    	 * 生成token签名EXPIRE_TIME 分钟后过期
    	 * 
    	 * @param username
    	 *            用户名
    	 * @return 加密的token
    	 */
    	public static String createJWT(String userId) {
    		...
    	}
    }
    

    多Realm配置

    用户密码认证和Jwt的认证需要不同的两个Realm,多Realm需要处理不同的Realm,获取到指定Realm的AuthenticationToken的数据模型。

    • 实现ModularRealmAuthenticator的方法
    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);
        }
    }
    
    • AuthenticatingRealm中实现getAuthenticationTokenClass方法
    public Class getAuthenticationTokenClass() {
        return JwtToken.class;
    }
    
    • 在SecurityManager中配置
    @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;
    }
    

    整合Swagger

    添加Swagger依赖

    <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>
    

    添加Swagger的配置

    @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("经供参考")
                    .version("1.0")
                    .build();
        }
    }
    

    总结

    在整个过程中,遇到的坑就是在Spring boot中Filter的自动注入,中间考虑有不使用注入的方式解决,即直接使用new JwtFilter()的方式,虽然也能解决问题,但是不是很完美,最终还是在网上找到解决方案。对Shiro的Filter链的执行过程加强了理解,能够使用自定的Filter解决实际问题。还有一个后续的问题,退出登录时的Jwt的token处理,它本身不能像Session一样,退出就清除,理论上只要没过期,就一直存在。可以考虑使用缓存,退出时清除即可,然后在校验时,先从缓存获取进行判断。

  • 相关阅读:
    CodeForces
    HDU
    HDU
    POJ
    URAL
    POJ
    UVa
    UVa
    UVa
    UVa
  • 原文地址:https://www.cnblogs.com/fzsyw/p/11405504.html
Copyright © 2011-2022 走看看