Source code project address

Try login times control implementation

Realize the principle of

Realm performs password matching when authenticating the user. The simplest case is a plaintext direct match, followed by an encrypted match, where the matching is done by CredentialsMatcher. Here we inherit this interface, define a password matcher, cache the key-value pair user name and the number of matches, if the password matches, delete the key-value pair, if the number of matches is not increased. Exceeding a given limit of times throws an error. Ehcache is used here.

Shiro – ehcache configuration

Maven rely on

< the dependency > < groupId > org, apache shiro < / groupId > < artifactId > shiro - ehcache < / artifactId > < version > 1.3.2 < / version > </dependency>Copy the code

Ehcache configuration

<? xml version="1.0" encoding="UTF-8"? > <ehcache name="es">

    <diskStore path="java.io.tmpdir"/ > <! -- name: indicates the cache name. MaxElementsInMemory: indicates the maximum number of caches. MaxElementsOnDisk: indicates the maximum number of caches on a hard disk. Eternal: Whether the object is permanent. Once it is set, timeout will not work. OverflowToDisk: whether to save to a disk when the system is down timeToIdleSeconds: Sets the time that the object is allowed to remain idle before becoming invalid (unit: second). Only when the eternal =falseOptional property for objects that are not permanently valid. The default value is 0, which means infinite idle time. TimeToLiveSeconds: Sets the time (in seconds) that the object is allowed to live before becoming invalid. The maximum time is between the creation time and the expiration time. Only when the eternal =falseIf the object is not permanently valid, the default is 0., that is, the object lifetime is infinite. DiskPersistent: Whether the disk store inside between restarts of the Virtual Machine. The default value is false. DiskSpoolBufferSizeMB: This parameter sets the size of the DiskStore cache. The default is 30MB. Each Cache should have its own buffer. DiskExpiryThreadIntervalSeconds: disk failure thread running time interval, the default is 120 seconds. MemoryStoreEvictionPolicy: when maxElementsInMemory limit is reached, Ehcache will be based on the specified strategies to clear the memory. The default policy is LRU (least recently used). You can set it to FIFO (First in, first out) or LFU (less used). ClearOnFlush: Specifies whether to flush the maximum memory. The three empty memoryStoreEvictionPolicy: Ehcache strategy; FIFO, the firstinFirst out, this is the most familiar, first out. LFU, Less Frequently Used, is the strategy Used in the above example. Frankly, it is the strategy that has been Used least. As mentioned above, cached elements have a hit attribute, and the one with the smallest hit value is cleared from the cache. The cached element has a timestamp. When the cache is full and space needs to be made for a new element, the element whose timestamp is furthest from the current time is removed from the cache. --> <defaultCache maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"/ > <! -- Logon record cache locked for 10 minutes --> <cache name="passwordRetryCache"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>

</ehcache>
Copy the code

#RetryLimitCredentialsMatcher

/** * validator, Strengthen the function of login number check such wrong password encryption * * @ author WGC * / @ Component public class RetryLimitCredentialsMatcher extends SimpleCredentialsMatcher { private static final Loggerlog = LoggerFactory.getLogger(RetryLimitCredentialsMatcher.class);

    private int maxRetryNum = 5;
    private EhCacheManager shiroEhcacheManager;

    public void setMaxRetryNum(int maxRetryNum) {
        this.maxRetryNum = maxRetryNum;
    }

    public RetryLimitCredentialsMatcher(EhCacheManager shiroEhcacheManager) {
    	this.shiroEhcacheManager = shiroEhcacheManager; 
    }
	
  
    @Override  
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {  
    	Cache<String, AtomicInteger> passwordRetryCache = shiroEhcacheManager.getCache("passwordRetryCache");
        String username = (String) token.getPrincipal();  
        //retry count + 1  
        AtomicInteger retryCount = passwordRetryCache.get(username);  
        if (null == retryCount) {  
            retryCount = new AtomicInteger(0);
            passwordRetryCache.put(username, retryCount);  
        }
        if (retryCount.incrementAndGet() > maxRetryNum) {
        	log.warn("User [{}] authenticates login.. Failed validation more than {} times", username, maxRetryNum);
            throw new ExcessiveAttemptsException("username: " + username + " tried to login more than 5 times in period");  
        }  
        boolean matches = super.doCredentialsMatch(token, info);  
        if (matches) {  
            //clear retry data  
        	passwordRetryCache.remove(username);  
        }  
        returnmatches; }}Copy the code

Shiro configuration modified

Injection CredentialsMatcher

/** * cache manager * @return cacheManager
     */
    @Bean
    public EhCacheManager ehCacheManager(){
        EhCacheManager cacheManager = new EhCacheManager();
        cacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
        returncacheManager; } /** * limit login times * @returnMatcher */ @bean public CredentialsMatcherretryLimitCredentialsMatcher() {
        RetryLimitCredentialsMatcher retryLimitCredentialsMatcher = new RetryLimitCredentialsMatcher(ehCacheManager());
        retryLimitCredentialsMatcher.setMaxRetryNum(5);
        return retryLimitCredentialsMatcher;

    }
Copy the code

Realm adds the authenticator

myShiroRealm.setCredentialsMatcher(retryLimitCredentialsMatcher());
Copy the code

Concurrent online number control implementation

KickoutSessionControlFilter

/ * * * * the number of concurrent login control @ author WGC * / public class KickoutSessionControlFilter extends AccessControlFilter {private static final Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class); /** * private String kickoutUrl; /** * kickoutAfter = / private Boolean kickoutAfter =false;
    /**
     * 同一个帐号最大会话数 默认1
     */
	private int maxSession = 1;

	private String kickoutAttrName = "kickout";
	
	private SessionManager sessionManager; 
	private Cache<String, Deque<Serializable>> cache; 
	
	public void setKickoutUrl(String kickoutUrl) { 
		this.kickoutUrl = kickoutUrl; 
	}
	
	public void setKickoutAfter(boolean kickoutAfter) { 
		this.kickoutAfter = kickoutAfter;
	}
	
	public void setMaxSession(int maxSession) { 
		this.maxSession = maxSession; 
	} 
	
	public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } /** * Sets the prefix of the Cache key */ public voidsetCacheManager(CacheManager cacheManager) { 
		this.cache = cacheManager.getCache("shiro-kickout-session");
	}
	
	@Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
			throws Exception {
		return false;
	} 
	
	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
			throws Exception { 
		Subject subject = getSubject(request, response); 
		if(! subject.isAuthenticated() && ! Subject. IsRemembered ()) {// If you are not logged in, proceed directly to the next processreturn true;
		} 
		
		Session session = subject.getSession();
		UserInfo user = (UserInfo) subject.getPrincipal(); 
		String username = user.getUsername();
		Serializable sessionId = session.getId();
		
		logger.info("KickoutControl, sessionId:{}", sessionId); <Serializable> Deque = cache.get(username);if(deque == null) { deque = new LinkedList<Serializable>(); cache.put(username, deque); } // If the sessionId does not exist in the queue and the user is not kicked out; In the queueif(! Deque. contains(sessionId) &&session. getAttribute(kickoutAttrName) == null) {// Store sessionId in queue deque.push(sessionId); } logger.info("deque.size:{}",deque.size()); // If the number of session ids in the queue exceeds the maximum number of sessions, start kicking peoplewhile(deque.size() > maxSession) { 
			Serializable kickoutSessionId = null; 
			if(kickoutAfter) {// kickoutSessionId = deque.removeFirst(); }else{// Otherwise kick the former kickoutSessionId = deque.removelast (); } // update the queue cache.put(username, deque); Try {/ / to get kicked out of the sessionId session object session kickoutSession = our sessionManager. GetSession (new DefaultSessionKey(kickoutSessionId));if(kickoutSession ! = null) {/ / set kickout properties of the session said kicked off the kickoutSession. SetAttribute (kickoutAttrName,true); } } catch (Exception e) { logger.error(e.getMessage()); }} // If kicked out, exit and redirect to the kicked addressif(session.getAttribute(kickoutAttrName) ! = null && (Boolean)session.getAttribute(kickoutAttrName) ==true) {// The session was kicked out of the try {// exit the logon subject.logout(); } catch (Exception e) { logger.warn(e.getMessage()); e.printStackTrace(); } saveRequest(request); // Redirect logger.info("User number of logins exceeded limit, redirected to {}", kickoutUrl);
			String reason = URLEncoder.encode("Account has exceeded the login limit."."UTF-8");
			String redirectUrl = kickoutUrl  + (kickoutUrl.contains("?")?"&" : "?") + "shiroLoginFailure=" + reason;  
			WebUtils.issueRedirect(request, response, redirectUrl); 
			return false;
		} 
		return true; }}Copy the code

Ehcache configuration

Ehcache – shiro. XML to join

<! -- user queue cache for 10 minutes --> <cache name="shiro-kickout-session"
           maxEntriesLocalHeap="2000"
           eternal="false"
           timeToIdleSeconds="3600"
           timeToLiveSeconds="0"
           overflowToDisk="false"
           statistics="true">
    </cache>
Copy the code

Shiro configuration

Inject related objects into shiroconfig.java

/** * Session manager * @return sessionManager
     */
    @Bean
    public DefaultWebSessionManager configWebSessionManager(){ DefaultWebSessionManager manager = new DefaultWebSessionManager(); // Add the cache manager manager.setCachemanager (ehCacheManager()); / / delete expired session manager. SetDeleteInvalidSessions (true); / / set the global session timeout manager. SetGlobalSessionTimeout (1 * 60 * 1000); / / whether regularly check the session manager. SetSessionValidationSchedulerEnabled (true);
        manager.setSessionValidationScheduler(configSessionValidationScheduler());
        manager.setSessionIdUrlRewritingEnabled(false);
        manager.setSessionIdCookieEnabled(true);
        returnmanager; } /** * session Session validation scheduler * @returnSession session authentication scheduler * / @ Bean public ExecutorServiceSessionValidationSchedulerconfigSessionValidationScheduler() { ExecutorServiceSessionValidationScheduler sessionValidationScheduler = new ExecutorServiceSessionValidationScheduler(); / / set the session invalidation scanning interval, milliseconds sessionValidationScheduler. SetInterval (300 * 1000);returnsessionValidationScheduler; } /** * Limit the number of simultaneous logins with the same account * @returnFilter * / @ Bean public KickoutSessionControlFilterkickoutSessionControlFilter() { KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); // Use cacheManager to obtain a cache to cache user login sessions. Used to preserve the user-session relationship; // We can use shiro redisManager() to implement the cacheManager() cache management. Reconfigure the cache time custom caching attributes such as kickoutSessionControlFilter. SetCacheManager (ehCacheManager ()); // It is used to get the session and kick it out according to the session ID. kickoutSessionControlFilter.setSessionManager(configWebSessionManager()); // Whether to kick out later logins, default is yesfalse; That is, the latter login user kicked out the former login user; Kick out of order. kickoutSessionControlFilter.setKickoutAfter(false); // Maximum number of sessions for a user. Default: 1; For example, "2" means a maximum of two users can log in at the same time. kickoutSessionControlFilter.setMaxSession(1); // Address redirected to after being kicked out; kickoutSessionControlFilter.setKickoutUrl("/login");
        return kickoutSessionControlFilter;
    }
Copy the code

Shiro filters the number of concurrent logins

filterChainDefinitionMap.put("/ * *"."kickout,user");
Copy the code

Access to any link requires authentication and limits the number of concurrent logins