文章目录

  • 前言
  • 模块结构
  • 设计思路
  • 配置
  • maven配置
  • WebSecurityConfigurerAdapter配置
  • 重写configure(HttpSecurity http)
  • 配置AuthenticationManager
  • 认证实现
  • 认证器的实现
  • 数据库账号密码实现
  • 钩子实现
  • 加解密实现
  • 授权实现
  • 鉴权过滤器实现
  • 签名实现
  • 接口权限实现
  • 如何使用FW-SECURITY


前言

前三张已经把应用框架的结构和最核心的Core模块已经分析完了,在实际开发中,我们只需要在我们的业务应用中引路核心maven最可以开始我们的业务开发了。Core处理了最基本的Web相关操作,如统一数据格式,统一异常,统一参数校验,统一上下文,解析器自定义;统一日志处理,统一日志(logback)配置;Mybatis配置封装,常用工具类;基础常量等实体;集成数据库版本管理(liqubase)定义了统一规范,也可灵活自定义;同时引入了nacos作为注册服务配置中心等。引入Core不论是单体应用还是微服务应用都能快速进行开发。读者也可以自己下载源码,进行自定义和扩展。

<dependency>
      <groupId>com.mars.fw</groupId>
      <artifactId>FW-CORE</artifactId>
      <version>1.0.0-SNAPSHOT</version>
 </dependency>

不论是做web开发,还是app开发都绕不开一个问题,认证和授权;我们可以使用比较流行的框架Shiro或者Spring-security又或是自己定义途径很多;这边我使用的是基于spring-security来进行封装和自定义的。首先先来看下模块的结构:整个FW-SECURITY

分成认证和授权两大块:图中已经很清晰的展现了各个模块的主要实现;授权这一块如果是基于微服务,可以在网关层做。

springboot未授权漏洞 springboot软件授权license_springboot未授权漏洞

模块结构

springboot未授权漏洞 springboot软件授权license_spring boot_02

设计思路

spring-security框架已经帮我们把,请求的整个路径处理的很好了,在请求链路中有去多前置和后置的handler实现,可以使用默认也可以我们自定义;和勾子很像,我们可以很轻松的通过配置,在我们想要的节点定义过滤器或者handler来实现我们自己的逻辑。并且它能够通过配置解决跨域等问题。下面我用流程图的方式来说明:

springboot未授权漏洞 springboot软件授权license_springboot未授权漏洞_03

配置

上面的图已经很清晰的展现了设计的思路,下面我们来看具体的实现。我将从配置说起,spring-security和spring-boot集成,所以我们只要简单的引入maven配置:

maven配置
<dependencies>
        <dependency>
            <groupId>com.mars.fw</groupId>
            <artifactId>FW-CORE</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
    </dependencies>
WebSecurityConfigurerAdapter配置

spring-security的所有配置都可以通过重写WebSecurityConfigurerAdapter的方法来配置。我们自定义一个类KingSecurityConfigure 继承WebSecurityConfigurerAdapter;接着我们来重写里面的部分方法。

重写configure(HttpSecurity http)

HttpSecurity对象是在WebSecurityConfigurerAdapter 的init的方法中获取的;具体的原理不在这边阐述,不然内容会太多,这边更多是以应用来阐述。下图是HttpSecurity和其他类的关系图,在init的方法中将HttpSecurity添加到securityFilterChainBuilders的list中,形成过滤器链。我们通过HttpSecurity对象可以像过滤链中注入我们自定义的过滤和各种配置,这个对象为我们提供了各种各样的入口

springboot未授权漏洞 springboot软件授权license_springboot未授权漏洞_04

package com.mars.fw.security;

import com.mars.fw.security.authentication.filter.CustomAuthenticationFilter;
import com.mars.fw.security.authentication.handler.AuthFailureHandler;
import com.mars.fw.security.authentication.handler.AuthSuccessHandler;
import com.mars.fw.security.authentication.handler.CustomLogoutSuccessHandler;
import com.mars.fw.security.authentication.provider.MobilePasswordAuthenticationProvider;
import com.mars.fw.security.authentication.provider.SmsAuthenticationProvider;
import com.mars.fw.security.authentication.provider.UserPasswordAuthenticationProvider;
import com.mars.fw.security.authentication.service.CustomUserDetailsService;
import com.mars.fw.security.authorization.filter.AuthenticationFilter;
import com.mars.fw.security.tool.SecurityConstant;
import com.mars.fw.security.tool.model.password.MD5PasswordEncoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.util.Arrays;

/**
 * @Author King
 * @create 2020/5/6 15:28
 */
@Configuration
@EnableWebSecurity
public class KingSecurityConfigure extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new MD5PasswordEncoder();
    }

    @Bean
    SmsAuthenticationProvider smsAuthenticationProvider() {
        return new SmsAuthenticationProvider();
    }

    @Bean
    MobilePasswordAuthenticationProvider emailAuthenticationProvider() {
        return new MobilePasswordAuthenticationProvider();
    }

    @Bean
    AuthenticationProvider customDaoAuthenticationProvider() {
        return new UserPasswordAuthenticationProvider(passwordEncoder(), userDetailsService);
    }

    public CustomAuthenticationFilter authenticationFilter() throws Exception {
        CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationSuccessHandler(new AuthSuccessHandler());
        filter.setAuthenticationFailureHandler(new AuthFailureHandler());
        return filter;
    }

    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        ProviderManager providerManager = new ProviderManager(Arrays.asList(customDaoAuthenticationProvider(), emailAuthenticationProvider(), smsAuthenticationProvider()));
        providerManager.setEraseCredentialsAfterAuthentication(false);
        return providerManager;
    }

    /**
     * 安全认证配置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable()
                .addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginProcessingUrl(SecurityConstant.LOGIN_ACTION)
                .and()
                .logout()
                .logoutUrl(SecurityConstant.LOGOUT_ACTION)
                .logoutSuccessHandler(new CustomLogoutSuccessHandler())
                .and()
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilter(new AuthenticationFilter(authenticationManager()));
    }
}
  • cors() 允许跨域处理,然后我们在新建一个配置类在里面注入CorsConfigurationSource的Bean,SpringSecurity会自动寻找name=corsConfigurationSource的Bean
  • springboot未授权漏洞 springboot软件授权license_spring_05

  • csrf() CSRF攻击处理
  • addFilterBefore(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
    在我们定义的authenticationFilter()在UsernamePasswordAuthenticationFilter过滤器之前执行
  • formLogin() 表单登录
  • loginProcessingUrl(SecurityConstant.LOGIN_ACTION) 登录的接口地址
  • logout() 允许注销
  • logoutUrl(SecurityConstant.LOGOUT_ACTION) 注销调用的地址
  • logoutSuccessHandler(new CustomLogoutSuccessHandler()) 注销成功的回调 在里面处理注销后的逻辑
  • authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated() 匹配?/下的所有请求都需要走认证接口
  • addFilter(new AuthenticationFilter(authenticationManager())) 在过滤器链中加入鉴权的过滤器,都会先及格过这个过滤器
配置AuthenticationManager

在spring-security的过滤器链路中,当拦截到登录接口的时候,它定义了一套类似责任链的模式来管理认证的数据流;除了最常用的UserDetailService的方式外。spring-security还定义了AuthenticationManager 来管理所有的登录方式,我们可以自定义各种provider来实现不同的认证逻辑,然后通过AuthenticationManager来统一管理,AuthenticationManager可以将所有的provider管理起来,可以定义只要其中一种provider通过认证就认证成功,否则按一定顺序找下一个provider,知道找到成功为止,或者全部失败则认证失败。原理可以在后续单独将这一块在来分析。

这边我定义了三个handler,具体的实现可以在源码中参考。

springboot未授权漏洞 springboot软件授权license_安全_06

认证实现

认证器的实现

上面讲到我定义了三个provider,分别是短信登录,用户名密码登录,手机密码登录,只要满足其中一种就可以登录。
我以现在最常用的短信登录为例子:SmsAuthenticationProvider,可以看到实际上是继承了一个AbstractAuthenticationProvider
AbstractAuthenticationProvider这个了也是我定义的做认证的公共实现。在虚类里面实现AuthenticationProvider接口,这个类里面有两个方法authenticate(Authentication authentication)和boolean supports(Class<?> authentication)
authenticate:是我们认证逻辑的实现
supports:自定义逻辑来开启是否开启这个认证逻辑,返回true的时候才会开启这个认证

package com.mars.fw.security.authentication.provider;

import com.mars.fw.security.tool.model.CustomAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;

/**
 * @description:定义一个自定义的provider的虚类做认证的公共实现
 * @author: aron
 * @date: 2019-07-03 15:45
 */
public abstract class AbstractAuthenticationProvider implements AuthenticationProvider {

    private CustomAuthenticationToken customAuthenticationToken;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        customAuthenticationToken = (CustomAuthenticationToken) authentication;
        return authenticateCustom(customAuthenticationToken);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }

    /**
     * 自定义认证方法 由子类实现
     *
     * @param customAuthenticationToken 自定义参数
     * @return 返回认证的结果 传递到链路中
     */
    protected abstract Authentication authenticateCustom(CustomAuthenticationToken customAuthenticationToken);
}

我们看下具体的实现,authenticateCustom 我们只要实现父类中的这个模板方法,里面的逻辑就不用多讲了短信验证的一个逻辑如果大家要使用短信,需要自己实现短信服务,后续在微服务中也会提供短息服务。

package com.mars.fw.security.authentication.provider;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.additional.query.impl.LambdaQueryChainWrapper;
import com.mars.fw.cache.CacheService;
import com.mars.fw.common.utils.SpringUtil;
import com.mars.fw.common.utils.StringUtils;
import com.mars.fw.security.authentication.exception.AuthenticationSmsException;
import com.mars.fw.security.authentication.service.CustomUserDetails;
import com.mars.fw.security.entity.UserEntity;
import com.mars.fw.security.mapper.KingUserMapper;
import com.mars.fw.security.tool.model.CustomAuthenticationToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * @description: 自定义短信验证码登录的provider
 * @author: aron
 * @date: 2019-04-20 09:46
 */
public class SmsAuthenticationProvider extends AbstractAuthenticationProvider {

    public static String SMS_CODE = "sms_code";

    @Autowired
    private KingUserMapper kingUserMapper;

    @Override
    protected Authentication authenticateCustom(CustomAuthenticationToken customAuthenticationToken) {
        try {
            String phone = (String) customAuthenticationToken.getPrincipal();
            String smsCode = (String) customAuthenticationToken.getCredentials();
            UserEntity userEntity = kingUserMapper.selectOne(new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getPhone, phone));
            if (userEntity == null) {
                throw new UsernameNotFoundException("手机用户不存在或该手机未绑定用户!");
            }
            //从缓存中获取手机号的短信验证码
            CacheService cacheService = SpringUtil.getBean(CacheService.class);
            Object object = cacheService.get(SMS_CODE + "_" + phone);
            if (null == object) {
                throw new AuthenticationSmsException("短信验证码已过期");
            }
            if (!smsCode.equals(object.toString())) {
                throw new AuthenticationSmsException("短信验证码错误");
            }
            //密码校验通过后 构建数据库用户信息
            CustomUserDetails userAccountDetails = new CustomUserDetails(userEntity);
            UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(userAccountDetails, customAuthenticationToken.getCredentials(), null);
            return result;
        } catch (Exception e) {
            throw new AuthenticationSmsException("手机短信验证登录失败");
        }
    }
}