①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
SpringSecurity 刚开始理解起来感觉很复杂,但是随着深入的了解,越来越喜欢了,为了更好的理解keycloak
如何跟 springsecurity 解释使用,这里把 springsecurity 的功能回顾一下。
任何安全系统都包含两个概念
SpringSecurity 通过一系列的 Filter 来进行权限认证
安全过滤器通过SecurityFilterChain API 插入到FilterChainProxy 中。过滤器的顺序很重要。通常不需要知道 Spring Security 的排序。然而,有时知道顺序是有益的Filter
Filter
以下是 Spring Security Filter 排序的完整列表:
UsernamePasswordAuthenticationFilter
DigestAuthenticationFilter
BasicAuthenticationFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
怎么替换默认过滤器?
http.addFilterAt() 不能替换默认的过滤器,只是在相同的位置放置一个过滤器,原本的过滤器仍然起作用。
可以 disable 掉默认的过滤器,例如用自定义的登出过滤器
http.logout().disable();
http.addFilterAt(new MyLogoutFilter(), LogoutFilter.class);
部分过滤器的含义:
ChannelProcessingFilter:转换协议时使用,例如将 http 重定向到 https
ConcurrentSessionFilter:判断 session 是否过期以及更新最新访问时间
SecurityContextPersistenceFilter:将用户信息绑定到线程
这样全局可通过 SecurityContextHolder.getContext().getAuthentication()拿到用户信息
注:如果要拿到 request/response 信息(这个不是过滤器设置的,是框架默认会绑定到线程)
可通过((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
HeaderWriterFilter:默认增加以下头部信息
X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY
关闭:http.headers().disable();
只保留缓存控制:http.headers().defaultsDisabled().cacheControl()
注:详细描述可查看官网地址
X-Content-Type-Options: nosniff 表示浏览器必须且只能根据 Content-Type 字段分辨资源类型(浏览器默认会 猜测资源类型)
X-XSS-Protection:防范 XSS 攻击
X-Frame-Options:是否允许页面被嵌套
**Cache-Control/Pragma/Expires:**缓存控制(简单描述)
CorsFilter: 配置跨域 ,可以通过 CorsConfigurationSource 来配置哪些允许跨域
CsrfFilter: 防止 Csrf 攻击,通过配置与 Session 绑定的 token,每次请求都需要携带最新的 token
注:默认是在前后端不分离的情况下,通过 jsp/ft 等传递到前端。在前后端分离的情况下,可以增加过滤器使其配置在 json/或者头部
LogoutFilter:配置登出的处理 一般可设置登出成功的处理,删除 cookie,使 Session 无效等
**UsernamePasswordAuthenticationFilter:**验证登录,并可以配置登录成功/失败的处理
SecurityContextHolderAwareRequestFilter: 包装类,实现 HttpServletRequest 的 getAuthentication getRemoteUser 等方法
RememberMeAuthenticationFilter: 配置 rememberMe,比如 7 天之内不需要登录
当拿不到用户信息时(SecurityContextHolder.getContext()为空),会去找 key 是 remember-me 的 Cookie 配置用户信息
AnonymousAuthenticationFilter:没有通过 username 和 remember 认证的用户赋予匿名身份
SessionManagementFilter:session 管理:限制同一用户开启多个会话的数量
ExceptionTranslationFilter:一般其只处理两大类异常:
AccessDeniedException 访问权限异常
AuthenticationException 用户认证异常:包括匿名用户异常
FilterSecurityInterceptor :拿到用户的权限(从 SecurityContextHolder 中获取 Authentication 对象)和资源所需权限(SecurityMetadataSource),在 AccessDecisionManager 里对比看用户是否有权限
ExceptionTranslationFilter
可以将 AccessDeniedException没有权限异常
或 AuthenticationException身份认证异常
转换成 HTTP responses.
AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
AccessDeniedException 用来解决认证过的用户访问无权限资源时的异常
在WebSecurityConfigurerAdapter
中添加这两个错误信息
httpSecurity.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
具体的错误处理,可以是跳转页面或者直接从 repose 中输出,这里展示跳出页面的例子。
@Log4j2public class CustomAccessDeniedHandler implements AccessDeniedHandler {public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)throws IOException, ServletException{log.debug(response.toString());//用response.sendRedirect 会重新定向//response.sendRedirect("/accessDenied");//这个不重新定向request.getRequestDispatcher("/accessDenied").forward(request,response);}}
Spring 官方的例子 Mfa 中也有一个用法,自定义了一个MfaTrustResolver
类,虽然没有认证成功,也会跳转到相应的页面中。
@BeanSecurityFilterChain web(HttpSecurity http,AuthorizationManager<RequestAuthorizationContext> mfaAuthorizationManager) throws Exception {MfaAuthenticationHandler mfaAuthenticationHandler = new MfaAuthenticationHandler("/second-factor");// @formatter:offhttp.authorizeHttpRequests((authorize) -> authorize.mvcMatchers("/second-factor", "/third-factor").access(mfaAuthorizationManager).anyRequest().authenticated()).formLogin((form) -> form.successHandler(mfaAuthenticationHandler).failureHandler(mfaAuthenticationHandler)).exceptionHandling((exceptions) -> exceptions.withObjectPostProcessor(new ObjectPostProcessor<ExceptionTranslationFilter>() {@Overridepublic <O extends ExceptionTranslationFilter> O postProcess(O filter) {filter.setAuthenticationTrustResolver(new MfaTrustResolver());return filter;}}));// @formatter:onreturn http.build();}
最常见的 Spring 可以通过权限、角色、IP 来进行授权。
前面登陆的例子,将一个权限分配到了这里
//2.得到UserDetailsUserDetailsImp user = new UserDetailsImp(dbUser.getUsername,dbUser.getUserpassword,AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal");)
配置一个权限或者多个
.antMatchers("/main1.html").hasAuthority("admin")多个.antMatchers("/main1.html").hasAnyAuthority("admin","dkdks")
角色必须要用 Role 开头
前面登陆的例子,将一个权限分配到了这里
//2.得到UserDetailsUserDetailsImp user = new UserDetailsImp(dbUser.getUsername,dbUser.getUserpassword,AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc");)
.antMatchers("/main1.html").hasRole("abc")配置多个.antMatchers("/main1.html").hasAnyRole("abc")
服务器端口,只能某个 IP 地址才可以调用
request.getRemoteAddr() //获取IP地址
可以这么来做
.antMatchers("/main.html").hasIpAdress("127.9.9.1");
做了一个拦截器 Filter,拦截 Request 中的 header,然后模拟登录。
在自定义的WebSecurityConfigurerAdapter
中添加 Filter 到用户密码验证之前。
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
这里有一个 Spring 的基础知识:Spring 默认的 Filter 加载顺序。
具体步骤如下:
AuthenticationToken
,这里会默认设置成已经登录setAuthenticated(true)
authenticated==true
,后续默认的 Spring 关于权限验证 Filter 才会放行。AuthenticationToken
放入SecurityContextHolder
这里采用了一种资源授权的模式,将所有的 URL 都设置成一个资源。
然后将一些资源编入到资源组中,或者也可以称作角色。,,并复制给登录用户。
在 config 文件中,为每个资源分配了权限,使用了hasAuthority
。 这里做的比较粗糙,是按照一个模块分配权限的,并没有细致区分:添加、删除、修改等权限。
/*** 数据库中得到访问权限的配置规则。* 数据库中给每个用户配置了可以访问的URL,所以这个URL是否可以访问,就匹配到这个权限。* 例如:用户A,配置了 /user/list。* 那么:httpSecurity.authorizeRequests().antMatchers(“api/admin/user/list”).hasAuthority("/user/list");* @param httpSecurity* @throws Exception*/private void setAuthFromRoute(HttpSecurity httpSecurity) throws Exception {List<AdminRoute> adminRoutes= adminRouteService.selectAll();adminRoutes.forEach(item->{try {// regexMatchers +"/*"String url=apiPrefix+item.getPath()+"/*";httpSecurity.authorizeRequests().antMatchers(url).hasAuthority(item.getPath());} catch (Exception e) {e.printStackTrace();}});}
Authentication
下面代码是在CustomUserDetailsService
中初始化 Authentication 的具体实现类UserDetails
@Overridepublic UserDetails loadUserByUsername(String username) {//查看是否已经缓存了String redisKey=getRedisKey(username);Object obj= redisTemplate.opsForValue().get(redisKey);if(obj!=null){return (UserDetails)obj;}Optional<Admin> user = adminService.selectByName(username);if(user.isEmpty()){throw new UsernameNotFoundException("用户名不存在");}else{List<GrantedAuthority> authorities = loadGrantedAuthorityByGroupId(user.get());UserDetails userDetails=new CustomUserDetails(user.get(),authorities);//缓存到redis中redisTemplate.opsForValue().set(redisKey,userDetails);return userDetails;}}