SpringBoot Security
数据库设计:
对象有三个:用户(user)、角色(role)、菜单(menu)。都为多对多的关联关系。菜单表的数据包括菜单ID、菜单的名称、权限码。
还可以设置一个用户组
的表,把用户进行分组,设计其他的业务需求。
访问一个接口流程:
- 用户登陆。返回 token
- 登陆后根据用户 ID 进行这三表关联查询。返回菜单列表 List,前端根据返回结果进行菜单显示,其他的隐藏掉。
- 请求接口把 token 放到头部,应用进行解析 token,获取用户 ID 进行查询校验权限码是否和接口中的硬编码是否一致。
整合
其中配置了自定义登录成功处理器、自定义登录失败处理器、自定义暂无权限处理器、自定义注销成功处理器、自定义未登录的处理器都是返回响应的 JSON 提示报文。userAuthenticationProvider
自定义登录逻辑验证器实现登陆的逻辑。自定义 PermissionEvaluator 实现具体的用户和权限的对比逻辑,前提要开启权限注解@EnableGlobalMethodSecurity(prePostEnabled = true)
默认是关闭的。
核心配置类:
/**
* 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 实现:
- 取 redis 中对应的 token,判断是否超时,超时提示超时,前端返回登陆页面
- 没超时的更新 redis 过期时间
分布式中:
用户权限认证应该是单独的一个模块。网关转发相关的请求到用户认证模块,得到确认的请求再转发到对应的的服务处理。服务间相互调用使用一个内部的 token,内部 token 跳过检验逻辑。
单项目运行过程,把权限写到代码里面通过校验数据库和代码上面的权限码进行判断。分布式中这样就不行了。不可能每个都整合SS
。可以通过设计表给 menu 表增加一个 uri 参数。记录每个请求地址对应的权限,不使用 SS 自定义权限注解验证。通过配置数据库的形式记录。判断符合请求路径有纪录并且权限码有的才通过校验。
额外思考:
- 每个请求都会访问认证服务。服务请求数据库频繁压力会很大。
可以缓存所有用户角色权限数据到内存中进行处理,减少与数据库的交互。
部署多个实例后,角色权限信息有新增修改会导致每个实例的内存数据不一致。
使用 zookeeper 的 watch 机制,每个实例启动时添加一个 zk watch,一旦节点数据改变,则更新内存数据。同时网关拦截响应的新增修改请求,对应修改节点数据达到通知的效果。