SpringSecurity基础

这里对SpringSecurity进行一个简单的整理。

参考来看,SpringSecurity是责任链模式,先认证(authentication),再授权(Authorization)。

changeLog:

  • 2021-04-15: 对比耿义的框架,重新整理

简介

参与要素

  • username用户
  • password密码
  • role(或者permission)
  • url

简介

认证:用户登录时,使用username与password登录,系统进行认证,认证通过之后,将user信息通过session保存

授权:在访问访问url时,系统根据url查找对应的role(或permission),然后再查看该用户是否具备该role(permission)

认证(authentication)

逻辑

登录

  • 用户登录,携带username + password
  • 后端根据username从数据库获取user信息,尤其是password
  • 对比password,相同则登录成功,生成token,并缓存信息;反之则认证失败

认证

  • 访问其他路由时,应携带token信息
  • 通过拦截器(或过滤器)拦截request请求,获取其中的token,没有返回401
  • 与查询缓存,是否有该token,没有则401,有则放行

流程

SpringSecurtity自己封装了login路由与对应的页面,它的过程如下:

  • 用户登录被AbstractAuthenticationProcessingFilter(UsernamePasswordAuthenticationFilter)拦截

    将username与password封装成Authentication对象

  • 将该Authentication对象传递给AuthenticationManager(chain)进行验证

    AuthenticationManager是接口,其实现是ProviderManager

  • ProviderManager调用AuthenticationProvider(handler)获取用户信息

    在DaoAuthenticationProvider(AuthenticationProvider的实现)中调用UserDetailsService去获取用户UserDetail,并进行对比。

  • 验证成功返回一个封装了权限信息(role或permission)的Authentication对象

    此时的Authentication包括着用户名、密码、权限信息

  • SpringSecurtiy框架将该对象放到SecurityContext中,使用时可以从SecurityContextHolder上下文中取出。

【缺少认证部分】

代码

登录拦截

UsernamePasswordAuthenticationFilter中进行了拦截,通过attemptAuthentication进行处理

public class UsernamePasswordAuthenticationFilter extends
      AbstractAuthenticationProcessingFilter {

   public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
   public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

   private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
   private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
   private boolean postOnly = true;

    // 构造器,以不区分大小写的方式post方式和HTTP方法创建匹配器。
   public UsernamePasswordAuthenticationFilter() {
      super(new AntPathRequestMatcher("/login", "POST"));
   }

   public Authentication attemptAuthentication(HttpServletRequest request,
         HttpServletResponse response) throws AuthenticationException {
      if (postOnly && !request.getMethod().equals("POST")) {
         throw new AuthenticationServiceException(
               "Authentication method not supported: " + request.getMethod());
      }
		
      // 从请求路径获取用户名和密码 
      String username = obtainUsername(request);
      String password = obtainPassword(request);

       // 空值判断
      if (username == null) {
         username = "";
      }

      if (password == null) {
         password = "";
      }

       // 去除用户名首尾空格
      username = username.trim();

       // 生成一个用户名密码身份验证的令牌
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);

      // 设置身份认证请求的信息
      setDetails(request, authRequest);

       // 返回一个完全经过身份验证的对象,包括凭据
      return this.getAuthenticationManager().authenticate(authRequest);
   }
    ....
    
    protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
}    

获取用户

这里还有一个UserDetailsService类,是框架用来获取用户信息的类,其只有一个接口loadUserByUsername,通过用户名获取用户的详细信息。

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    MyUserMapper mapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        MyUserBean userBean = mapper.selectByUsername(username);
        if (userBean == null) {
            throw new UsernameNotFoundException("数据库中无此用户!");
        }
        return userBean;
    }
}

通过AuthenticationProvider与UserDetailsService都通过SpringConfiguration放到配置中

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    BackdoorAuthenticationProvider backdoorAuthenticationProvider;
    
     @Autowired
    MyAccessDeniedHandler myAccessDeniedHandler;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       ...省略
        //将自定义验证类注册进去
        auth.authenticationProvider(backdoorAuthenticationProvider);
        auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
  ...省略
}

密码验证

这里稍微有点疑问,从代码看,可以通过AuthenticationProvider手动写代码进行验证,如果通过返回Authentication类,否则不返回;而这里添加的UserDetailsService是根据用户名获取数据的,显然也是为了对比密码做准备的。疑问是,密码对比到底在哪进行?

通过断点发现,UserDetailService 是由DaoAuthenticationProvider调用,而它是AbstractUserDetailsAuthenticationProvider的一个子类,在AbstractUserDetailsAuthenticationProvider::authenticate中,调用了子类的DaoAuthenticationProvider::retrieveUser()函数,由此获取用户的信息,然后调用了additionalAuthenticationChecks来实现,而这个方法是有DaoAuthenticationProvider实现的。

这是典型的template模式。

看看这段代码。

class AbstractUserDetailsAuthenticationProvider{
    
    public Authentication authenticate(Authentication authentication){
        //...省略
        // 获取用户,若不存在,则抛异常
        try {
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
					//..省略
					throw notFound;
			}
        
           //...省略
 		  //additionalAuthenticationChecks,将用户信息与anthentication传入,进行验证。
		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
				//...省略
				throw exception;
		}
       // ...省略      
    }
    
    // ...省略
    this.createSuccessAuthentication(principalToReturn, authentication, user);
}

在来看看密码验证的实现:

class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {	
    	private PasswordEncoder passwordEncoder;
    		private UserDetailsService userDetailsService;
    
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		//...省略
		String presentedPassword = authentication.getCredentials().toString();

		if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			logger.debug("Authentication failed: password does not match stored value");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}
	}
    
    
    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;
		}
		//...省略
	}

}

示例

通过Provider增加admin后门

业务逻辑基本都是在AuthenticationProvider中实现,然后通过Security的配置,加入到验证链中。

@Component
public class BackdoorAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String name = authentication.getName();
        String password = authentication.getCredentials().toString();

        //利用alex用户名登录,不管密码是什么都可以,伪装成admin用户
        if (name.equals("admin")) {
            Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
            authorityCollection.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            authorityCollection.add(new SimpleGrantedAuthority("ROLE_USER"));
            return new UsernamePasswordAuthenticationToken(
                    "admin", password, authorityCollection);
        } else {
            return null;
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(
                UsernamePasswordAuthenticationToken.class);
    }
}

绕过自带login的方法

PS:对于使用security这种方法麻烦

  1. 如果要绕过自带的login,需要自己实现login路由,login成功之后,生成token并缓存。因为login自己实现了,所以框架主要做的就是认证部分。

  2. 重实现AbstractAuthenticationProcessingFilter来取代框架自带的UsernamePasswordAuthenticationFilter.封装auth对象时,仅存token即可

    ublic class TokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
        private static final String BEARER = "Bearer";
    
        public  TokenAuthenticationFilter(final RequestMatcher requiresAuth) {
            super(requiresAuth);
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
            final String param = Optional.ofNullable(request.getHeader(AUTHORIZATION)).orElse(request.getParameter("t"));
    
            final String token = Optional.ofNullable(param)
    //                .map(value-> removeStart(value, BEARER))
                    .map(String::trim)
                    .orElseThrow(()->new BadCredentialsException("Missing Authentication Token"));
    
            Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
            return getAuthenticationManager().authenticate(auth);
        }
        ...
    }
    
  3. 实现AbstractUserDetailsAuthenticationProvider来获取缓存中的用户

    tokenService是从redis缓存中通过token去获取用户

    public class TokenAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    
        @Autowired
        TokenService tokenService;
    
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        }
    
        @Override
        protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    
            final Object token = authentication.getPrincipal();
            return Optional
                    .ofNullable(token)
                    .map(String::valueOf)
                    .flatMap(tokenService::getUserByToken)
                    .orElseThrow(()->new UsernameNotFoundException("Cannot find user with authentication token=" + token));
        }
    

授权(authorization)

逻辑

授权是在认证的基础之上的

  • 用户访问路由时,通过拦截器进行拦截

  • 获取访问的路由所需要的权限

  • 根据认证的结果(SecurityContext中),找到访问者所拥有的权限

  • 依据策略,对别这两个权限,通过则放行

流程

  • 访问url时,会通过AbstractSecurityInterceptor拦截器拦截

  • 它会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限

  • 然后调用授权管理器AccessDecisionManager来进行验证

    这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,然后根据所配的策略(有:一票决定,一票否定,少数服从多数等)进行验证。如果权限足够,则返回,权限不够则报错。

这里有点模板模式的意思,但更复杂一些。

调用的入口应该是FilterSecurityIntercepor的invoke()函数,在这个函数中,调用了父函数的beforeInvocation()、finallyInvocation()、afterInvocation()。对授权的处理,就在AbstractSecurityInterceptor::beforeInvocation()中实现的。

在这个函数中,通过调用obtainSecurityMetadataSource()获取了FilterSecurityInterceptor中的对象(自己实现的),通过它获取了与url匹配权限。然后将数据交给了AccessDesionManager(自己实现)的decide()函数判断是否通过。

代码

【缺少后2部分的源码】

AbstractSecurityInterceptor

AbstractSecurityInterceptor::beforeInvocation中的调用情况

	protected InterceptorStatusToken beforeInvocation(Object object) {

		...
         // 调用了MySecurityMetadataSource
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);

		// 获取用户的Authentication
		Authentication authenticated = authenticateIfRequired();
	
		// 调用decide
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));

			throw accessDeniedException;
		}
		....
	}

通过url获取所需要的权限

@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    ResourceMapper resourceMapper;
    //本方法返回访问资源所需的角色集合
    AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        //从object中得到需要访问的资源,即网址
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        
        //从数据库中得到所有资源,以及对应的角色
        List<MyResourceBean> resourceBeans = resourceMapper.selectAllResource();
        
        for (MyResourceBean resource : resourceBeans) {
            //首先进行地址匹配
            if (antPathMatcher.match(resource.getUrl(), requestUrl)
                    && resource.getRolesArray().length > 0) {
                return SecurityConfig.createList(resource.getRolesArray());
            }
        }
        //匹配不成功返回一个特殊的ROLE_NONE
        return SecurityConfig.createList("ROLE_NONE");
    }
	...
}

通过AccessDecisionManager鉴权

看看decide

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        //从authentication中获取当前用户具有的角色
        Collection<? extends GrantedAuthority> userAuthorities = authentication.getAuthorities();

        //从configAttributes中获取访问资源所需要的角色,它来自MySecurityMetadataSource的getAttributes
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute attribute = iterator.next();
            String role = attribute.getAttribute();

            if ("ROLE_NONE".equals(role)) {
                if (authentication instanceof AnonymousAuthenticationToken) {
                    throw new BadCredentialsException("用户未登录");
                } else
                    return;
            }
            //逐一进行角色匹配
            for (GrantedAuthority authority : userAuthorities) {
                if (authority.getAuthority().equals("ROLE_ADMIN")) {
                    return; //用户具有ROLE_ADMIN权限,则可以访问所有资源
                }
                if (authority.getAuthority().equals(role)) {
                    return;  //匹配成功就直接返回
                }
            }
        }
        //不能完成匹配
        throw new AccessDeniedException("你没有访问" + object + "的权限!");
    }
}

示例

注解方式明确路由的权限

按照security的使用方式,授权被框架封装起来了,只需要在路由上增加注解,说明需要的权限即可。

    @ApiOperation("查询部门")
    @GetMapping("/api/dept/{id}")
    @PreAuthorize("@el.check('user:list','admin','dept:detail')")
    public ResponseEntity<Object> getDeptById(@PathVariable Long id) {
      	...
    }

这个el.check()如下:

@Service(value = "el")
public class ElPermissionConfig {
    public Boolean check(String... permissions) {
        // 获取当前用户的所有权限
        List<String> elPermissions = SecurityUtils.getUserDetails().getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
        // 判断当前用户的所有权限是否包含接口上定义的权限
        return elPermissions.contains("admin") || Arrays.stream(permissions).anyMatch(elPermissions::contains);
    }
}

不在需要FilterInvocationSecurityMetadataSource与AccessDecisionManager的内容。

这种具体的实现方式待实践。

数据库方式

如果需要路由与权限的信息存储到数据库中,重新实现FilterInvocationSecurityMetadataSource、AccessDecisionManager

通过实现FilterInvocationSecurityMetadataSource从数据库中读取权限

@Component
public class SecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    SourceService sourceService;

    AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        String method = ((FilterInvocation) object).getRequest().getMethod();
        List<Source> sourceList = sourceService.findSourceByMethod(method);
        List<String> permissionList = new ArrayList<>();
        for (Source source: sourceList) {
            if(antPathMatcher.match(source.getUrl(), requestUrl) && !source.getPermissions().isEmpty()){
                permissionList.add(source.getPermissions());
            }
        }

//        if(permissionList.size() == 0){
//            permissionList.add("NONE");
//        }

        String[] str = new String[permissionList.size()];
        for (int i = 0; i < permissionList.size(); i++) {
            str[i] = permissionList.get(i);
        }
        return SecurityConfig.createList(str);
    }
    ...
}

通过实现AccessDecisionManager来进行鉴权

public class MyAccessDecisionManager implements AccessDecisionManager {

    @Autowired
    TokenService tokenService;

    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
        User user = (User)authentication.getPrincipal();

        Iterator<ConfigAttribute> iterator = collection.iterator();
        List<String> permissions = new ArrayList<>();
        while (iterator.hasNext()){
            ConfigAttribute attribute = iterator.next();
            permissions.addAll(Arrays.asList(attribute.getAttribute().split(";")));
        }

        if(permissions.size() == 0){
            return;
        }
        for (String userPermission: user.getPermissionList()) {
            if(permissions.contains(userPermission)){
                return;
            }
        }

        throw new AccessDeniedException("你没有访问权限!");
    }

这两个类也是通过SecurityConfiguration配置进了框架

@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    BackdoorAuthenticationProvider backdoorAuthenticationProvider;
     @Autowired
    MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    MyAccessDecisionManager myAccessDecisionManager;
    @Autowired
    MySecurityMetadataSource mySecurityMetadataSource;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       ...省略
        //将自定义验证类注册进去
        auth.authenticationProvider(backdoorAuthenticationProvider);
        auth.userDetailsService(myUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(mySecurityMetadataSource);
                        object.setAccessDecisionManager(myAccessDecisionManager);
                        return object;
                    }
                })
                .and()
                .csrf().disable()
                .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
    }
  ...省略
}

【SecurityContext存在哪里?】

# spring 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×