SpringBoot Security

数据库设计:

1580536136205.png

对象有三个:用户(user)、角色(role)、菜单(menu)。都为多对多的关联关系。菜单表的数据包括菜单ID、菜单的名称、权限码。

还可以设置一个用户组的表,把用户进行分组,设计其他的业务需求。

访问一个接口流程:

  1. 用户登陆。返回 token
  2. 登陆后根据用户 ID 进行这三表关联查询。返回菜单列表 List,前端根据返回结果进行菜单显示,其他的隐藏掉。
  3. 请求接口把 token 放到头部,应用进行解析 token,获取用户 ID 进行查询校验权限码是否和接口中的硬编码是否一致。

整合

其中配置了自定义登录成功处理器、自定义登录失败处理器、自定义暂无权限处理器、自定义注销成功处理器、自定义未登录的处理器都是返回响应的 JSON 提示报文。userAuthenticationProvider自定义登录逻辑验证器实现登陆的逻辑。自定义 PermissionEvaluator 实现具体的用户和权限的对比逻辑,前提要开启权限注解@EnableGlobalMethodSecurity(prePostEnabled = true) 默认是关闭的。

GitHub

核心配置类:

/**
 * SpringSecurity配置类
 * @Author Sans
 * @CreateTime 2019/10/1 9:40
 */
@Configuration
@EnableWebSecurity
// @EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * 自定义登录成功处理器
     */
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    /**
     * 自定义登录失败处理器
     */
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    /**
     * 自定义注销成功处理器
     */
    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;
    /**
     * 自定义暂无权限处理器
     */
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    /**
     * 自定义未登录的处理器
     */
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    /**
     * 自定义登录逻辑验证器
     */
    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;

    /**
     * 加密方式
     * @Author Sans
     * @CreateTime 2019/10/1 14:00
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 注入自定义PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }

    /**
     * 配置登录验证逻辑
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        //这里可启用我们自己的登陆验证逻辑
        auth.authenticationProvider(userAuthenticationProvider);
    }
    /**
     * 配置security的控制逻辑
     * @Author Sans
     * @CreateTime 2019/10/1 16:56
     * @Param  http 请求
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 不进行权限验证的请求或资源(从配置文件中读取)
               .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                // 其他的需要登陆后才能访问
                .anyRequest().authenticated()
                .and()
                // 配置未登录自定义处理类
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                // 配置登录地址
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                // 配置登录成功自定义处理类
                .successHandler(userLoginSuccessHandler)
                // 配置登录失败自定义处理类
                .failureHandler(userLoginFailureHandler)
                .and()
                // 配置登出地址
                .logout()
                .logoutUrl("/login/userLogout")
                // .logoutSuccessUrl("logout.html")
                // 配置用户登出自定义处理类
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                // 配置没有权限自定义处理类
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                // 开启跨域
                .cors()
                .and()
                // 取消跨站请求伪造防护
                .csrf().disable();
        // 基于Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用缓存
        http.headers().cacheControl();
        // 添加JWT过滤器
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

这里使用的是 JWT 处理 token,要考虑 token 过期刷新的问题,很恶心。

建议自己实现 token 过滤器和 userLogoutSuccessHandler(实现生成包含用户信息、加密的 token)。

JWTAuthenticationTokenFilter 可以使用 redis 实现:

  1. 取 redis 中对应的 token,判断是否超时,超时提示超时,前端返回登陆页面
  2. 没超时的更新 redis 过期时间

分布式中:

用户权限认证应该是单独的一个模块。网关转发相关的请求到用户认证模块,得到确认的请求再转发到对应的的服务处理。服务间相互调用使用一个内部的 token,内部 token 跳过检验逻辑。

单项目运行过程,把权限写到代码里面通过校验数据库和代码上面的权限码进行判断。分布式中这样就不行了。不可能每个都整合SS。可以通过设计表给 menu 表增加一个 uri 参数。记录每个请求地址对应的权限,不使用 SS 自定义权限注解验证。通过配置数据库的形式记录。判断符合请求路径有纪录并且权限码有的才通过校验。

1580542571188.png

额外思考:

  1. 每个请求都会访问认证服务。服务请求数据库频繁压力会很大。

​ 可以缓存所有用户角色权限数据到内存中进行处理,减少与数据库的交互。

  1. 部署多个实例后,角色权限信息有新增修改会导致每个实例的内存数据不一致。

    使用 zookeeper 的 watch 机制,每个实例启动时添加一个 zk watch,一旦节点数据改变,则更新内存数据。同时网关拦截响应的新增修改请求,对应修改节点数据达到通知的效果。

上次更新时间: 2024/5/7 05:59:02