公司用的是若依管理系统封装了SpringBoot,权限使用了SpringSecurity+JWT令牌机制,今天花时间研究了一下若依是如何对Security进行封装的。

登录

1.用户存在性验证

首先将后台清了一下,访问系统登录的页面,发现控制台打印了如下SQL:

16:10:13.119 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - ==>  Preparing: select u.user_id, u.dept_id, u.user_name, u.nick_name, u.user_type, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, u.entry_time, u.staff_type, u.salary_base, u.is_calc, d.dept_id, d.parent_id, d.dept_name, d.order_num, d.leader, d.status as dept_status,r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status from sys_user u left join sys_dept d on u.dept_id = d.dept_id left join sys_user_role ur on u.user_id = ur.user_id left join sys_role r on r.role_id = ur.role_id where u.user_name = ? 
16:10:13.120 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.123 [http-nio-8082-exec-15] DEBUG c.r.s.m.S.selectUserByUserName - [debug,159] - <==      Total: 1
16:10:13.246 [schedule-pool-3] INFO  sys-user - [run,54] - [192.168.*.***]内网IP[admin][Success][登录成功]
16:10:13.246 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - ==>  Preparing: insert into sys_logininfor (user_name, status, ipaddr, login_location, browser, os, msg, login_time) values (?, ?, ?, ?, ?, ?, ?, sysdate()) 
16:10:13.247 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - ==> Parameters: admin(String), 0(String), 192.168.*.***(String), 内网IP(String), Chrome 8(String), Windows 10(String), 登录成功(String)
16:10:13.250 [schedule-pool-3] DEBUG c.r.s.m.S.insertLogininfor - [debug,159] - <==    Updates: 1
16:10:13.297 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==>  Preparing: select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0 and m.visible = 0 order by m.parent_id, m.order_num 
16:10:13.298 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Parameters: 
16:10:13.304 [http-nio-8082-exec-19] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - <==      Total: 128
16:10:13.486 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==>  Preparing: select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.menu_type, m.icon, m.order_num, m.create_time from sys_menu m where m.menu_type in ('M', 'C') and m.status = 0 and m.visible = 0 order by m.parent_id, m.order_num 
16:10:13.487 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - ==> Parameters: 
16:10:13.491 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - ==>  Preparing: select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status, r.del_flag, r.create_time, r.remark from sys_role r left join sys_user_role ur on ur.role_id = r.role_id left join sys_user u on u.user_id = ur.user_id left join sys_dept d on u.dept_id = d.dept_id WHERE r.del_flag = '0' and u.user_name = ? 
16:10:13.492 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.495 [http-nio-8082-exec-22] DEBUG c.r.s.m.S.selectMenuTreeAll - [debug,159] - <==      Total: 128
16:10:13.499 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectRolesByUserName - [debug,159] - <==      Total: 1
16:10:13.500 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - ==>  Preparing: select p.post_id, p.post_name, p.post_code from sys_post p left join sys_user_post up on up.post_id = p.post_id left join sys_user u on u.user_id = up.user_id where u.user_name = ? 
16:10:13.501 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - ==> Parameters: admin(String)
16:10:13.503 [http-nio-8082-exec-23] DEBUG c.r.s.m.S.selectPostsByUserName - [debug,159] - <==      Total: 1

一共6条sql语句,首先第一条查的是用户表的用户信息,于是就去看了一下数据库,将第一条sql又执行了一遍,结果如下:
在这里插入图片描述
可见密码是经过加密了,于是又去看login接口是怎么实现的,如下:


@Component
public class SysLoginController{
	@PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }
}
	

可见核心方法在于 String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
于是开启断点追踪下去:loginService的login方法是这么实现的:

public class SysLoginService{
	@Resource
    private AuthenticationManager authenticationManager;
/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @return 结果
     */
    public String login(String username, String password){
        String verifyKey = Constants.CAPTCHA_CODE_KEY;
        String captcha = redisCache.getCacheObject(verifyKey);
        redisCache.deleteObject(verifyKey);
//        if (captcha == null){
//            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
//            throw new CaptchaExpireException();
//        }
//        if (!code.equalsIgnoreCase(captcha)){
//            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
//            throw new CaptchaException();
//        }
        // 用户验证
        Authentication authentication = null;
        try{
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        catch (Exception e){
            if (e instanceof BadCredentialsException){
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else{
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new CustomException(e.getMessage());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser);
    }
}

注释的那部分是用于验证验证码的部分,断点继续往下走,登录的核心部分是,这也是SpringSecurity用于登录的核心方法.

// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

关于AuthenticationManager,它是SpringSecurity的身份验证管理器,AuthenticationManager的authenticate()方法用于登录验证,其参数UsernamePasswordAuthenticationToken无疑是封装了用户登录信息的一个对象,于是现在开始进入到了SpringSecurity的环节,AuthenticationManager是一个接口,所以authenticate()方法必定交给它的实现类来完成,通过debug可以看见Spring为其注入的是WebSecurityConfigurerAdapter
在这里插入图片描述
那么问题来了,通过继承树发现WebSecurityConfigurerAdapter并非AuthenticationManager的实现类,那这就出事了,先不管,继续往下走
于是再进入authenticationManager.authenticate()方法中,调用者是WebSecurityConfigurerAdapter但是他有一个静态内部类AuthenticationManagerDelegator,而AuthenticationManagerDelegator实现了AuthenticationManager其实现的方法为

		public Authentication authenticate(Authentication authentication)
				throws AuthenticationException {
			if (delegate != null) {
				return delegate.authenticate(authentication);
			}

			synchronized (delegateMonitor) {
				if (delegate == null) {
					delegate = this.delegateBuilder.getObject();
					this.delegateBuilder = null;
				}
			}

			return delegate.authenticate(authentication);
		}

这还是看不懂,继续走,于是再进入到delegate.authenticate(authentication);方法中,其实现如下:

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}

还是不懂,继续往里走,进入result = provider.authenticate(authentication);方法中(中间过程还有一步父子类的共同实现,但是是同一个方法的不同属性值,逻辑一样),如下:

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// Determine username
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(username);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

这仿佛一下子就开朗了,登录信息一直封装再参数authentication中,再次比对往下走,进入到了
retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);方法中,该方法的逻辑为

	protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

这一下子就出来了,UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);不正是去取数据库中的用户信息吗,且该处若出现异常,立即抛出UsernameNotFoundException,正对应上了之前的判断,在该断点处对该方法放行,果然控制台打印出了sql语句,真是登陆时的第一条sql,再次进入到该方法中,发现

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        SysUser user = userService.selectUserByUserName(username);
        if (StringUtils.isNull(user)){
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())){
            log.info("登录用户:{} 已被删除.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus())){
            log.info("登录用户:{} 已被停用.", username);
            throw new BaseException("对不起,您的账号:" + username + " 已停用");
        }
        return createLoginUser(user);
    }

这就一一对应上了

Logo

快速构建 Web 应用程序

更多推荐