JWT permission source sharing part2 in B2B2C system

Time:2020-9-26

We shared the basic design and source code of JWT permission in B2B2C system with you in the previous article “JWT permission source sharing Part1 of B2B2C system”. This article continues to share with you the ideas and source code of JWT and spring security integration.

In the last article, we shared the key class diagrams:

 

As shown in the figure above, permission verification mainly involves four categories:

  • AbstractAuthenticationService

  • BuyerAuthenticationService

  • SellerAuthenticationService

  • AdminAuthenticationService

 

AbstractAuthenticationService

For the common part of the three-terminal (Buyer Buyer Management) verification, we abstract it in abstractauthentication service:

public abstract class AbstractAuthenticationService implements AuthenticationService {

    @Autowired
    protected TokenManager tokenManager;


    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     *Singleton mode cache
     */
    private static Cache cache;


    @Autowired
    private JavashopConfig javashopConfig;


    /**
     *For authentication, get the token first, and then authenticate according to the token
     *The production environment needs to obtain the token by nonce, time stamp and signature
     *The development environment can transfer token directly
     *
     * @param req
     */
    @Override
    public void auth(HttpServletRequest req) {
        String token = this.getToken(req);
        if (StringUtil.notEmpty(token)) {
            Authentication authentication = getAuthentication(token);
            if (authentication != null) {
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }

        }
    }

    /**
     *Receive user disable or unblock events
     *Disable: writes the disabled user ID to the cache
     *Unblock: delete the user ID stored in the cache
     *
     * @param userDisableMsg
     */
    @Override
    public void userDisableEvent(UserDisableMsg userDisableMsg) {

        //Logging users in the cache is disabled
        Cache cache = this.getCache();

        if (UserDisableMsg.ADD.equals(userDisableMsg.getOperation())) {
            logger.debug ("received user disable message: + userdisablemsg)";
            cache.put(getKey(userDisableMsg.getRole(), userDisableMsg.getUid()), 1);
        }

        if (UserDisableMsg.DELETE.equals(userDisableMsg.getOperation())) {
            logger.debug ("received user unblock message: + userdisablemsg)";
            cache.remove(getKey(userDisableMsg.getRole(), userDisableMsg.getUid()), 1);
        }
    }

    protected void checkUserDisable(Role role, int uid) {
        Cache cache = this.getCache();
        Integer isDisable = cache.get(getKey(role, uid));
        if (isDisable == null) {
            return;
        }
        if (1 == isDisable) {
            Throw new runtimeException ("user has been disabled");
        }
    }

    private String getKey(Role role, int uid) {

        return role.name() + "_" + uid;
    }

    /**
     *Parsing a token
     *The subclass needs to parse the token into its own sub business permission model: admin, seller, buyer
     *
     * @param token
     * @return
     */
    protected abstract AuthUser parseToken(String token);

    /**
     *Generate authorization based on a token
     *
     * @param token
     *@ return authorization
     */
    protected Authentication getAuthentication(String token) {
        try {

            AuthUser user = parseToken(token);
            List auths = new ArrayList<>();

            List roles = user.getRoles();

            for (String role : roles) {
                auths.add(new SimpleGrantedAuthority("ROLE_" + role));
            }

            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken("user", null, auths);
            authentication.setDetails(user);

            return authentication;
        } catch (Exception e) {
            logger.error ("authentication exception", e);
            return new UsernamePasswordAuthenticationToken("anonymous", null);
        }
    }

    /**
     *Get token
     *7.2.0, discard the judgment of replay attack
     *
     * @param req
     * @return
     */
    protected String getToken(HttpServletRequest req) {

        String token = req.getHeader(TokenConstant.HEADER_STRING);
        if (StringUtil.notEmpty(token)) {
            token = token.replaceAll(TokenConstant.TOKEN_PREFIX, "").trim();
        }

        return token;
    }

    private static final Object lock = new Object();

    /**
     *Get local cache
     *Used to record disabled users
     *The key of this cache is: role + user ID, for example: admin_1
     *If the value is: 1, the user is disabled
     *
     * @return
     */
    protected Cache getCache() {

        if (cache != null) {
            return cache;
        }
        synchronized (lock) {
            if (cache != null) {
                return cache;
            }
            //The cache time is session validity + one minute
            //In other words, if the user is disabled, the session timeout will not be needed
            //Because he needs to log in again to be detected invalid
            int sessionTimeout = javashopConfig.getRefreshTokenTimeout() - javashopConfig.getAccessTokenTimeout() + 60;

            //Using ehcache as cache
            CachingProvider provider = Caching.getCachingProvider("org.ehcache.jsr107.EhcacheCachingProvider");
            CacheManager cacheManager = provider.getCacheManager();

            MutableConfiguration configuration =
                    new MutableConfiguration()
                            .setTypes(String.class, Integer.class)
                            .setStoreByValue(false)
                            .setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(new Duration(TimeUnit.SECONDS, sessionTimeout)));

            cache = cacheManager.createCache("userDisable", configuration);

            return cache;
        }
    }

}

 

In javashop B2B2C system, if a user is disabled, the user is required to be unable to operate immediately

In the method of checkuserdisable, the idea is to put disabled users in local cache by listening to redis messages (ehcache is used here).

 

BuyerAuthenticationService

With the previous code foundation, the three terminal permission verification is relatively simple:

 

@Component
public class BuyerAuthenticationService extends AbstractAuthenticationService {

    @Override
    protected AuthUser parseToken(String token) {
        AuthUser authUser=  tokenManager.parse(Buyer.class, token);
        User  user = (User) authUser;
        checkUserDisable(Role.BUYER, user.getUid());
        return authUser;
    }

}
 

 

 

SellerAuthenticationService

@Component
public class SellerAuthenticationService extends AbstractAuthenticationService {

    /**
     *Resolve token to clerk
     *
     * @param token
     * @return
     */
    @Override
    protected AuthUser parseToken(String token) {
        AuthUser authUser = tokenManager.parse(Clerk.class, token);
        User user = (User) authUser;
        checkUserDisable(Role.CLERK, user.getUid());
        return authUser;
    }

}

 

AdminAuthenticationService

 

@Component
public class AdminAuthenticationService extends AbstractAuthenticationService {


    /**
     *Resolve token to admin
     * @param token
     * @return
     */
    @Override
    protected AuthUser parseToken(String token) {

        AuthUser authUser=  tokenManager.parse(Admin.class, token);
        User user = (User) authUser;
        checkUserDisable(Role.ADMIN, user.getUid());
        return authUser;

    }

}

 

 

Integrating security:

 

@Configuration
@EnableWebSecurity
public class BuyerSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private DomainHelper domainHelper;

    @Autowired
    private BuyerAuthenticationService buyerAuthenticationService;

    @Autowired
    private AccessDeniedHandler accessDeniedHandler;

    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;




    /**
     *Define permissions for seller projects
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.cors().configurationSource((CorsConfigurationSource) ApplicationContextHolder.getBean("corsConfigurationSource")).and().csrf().disable()
                //Disable session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                //Define the format of failure to verify
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint).and()
                .authorizeRequests()
                .and()
                .addFilterBefore(new TokenAuthenticationFilter(buyerAuthenticationService),
                        UsernamePasswordAuthenticationFilter.class);

        //Filter out the swagger's path
        http.authorizeRequests().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources", "/configuration/security", "/swagger-ui.html", "/webjars/**").anonymous();
        //Filter out APIs that do not require buyer permission
        http.authorizeRequests().antMatchers("/debugger/**" ).permitAll().and();
        //Definition can only be accessed with buyer permission
        http.authorizeRequests().anyRequest().hasRole(Role.BUYER.name());
        http.headers().addHeaderWriter(xFrameOptionsHeaderWriter());
        //Disable caching
        http.headers().cacheControl().and()
                .contentSecurityPolicy("script-src  'self' 'unsafe-inline' ; frame-ancestors " + domainHelper.getBuyerDomain());

    }

 

 

The above is the sharing of permissions in the source code of javashop e-commerce system.