Review

①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖

SpringSecurity 刚开始理解起来感觉很复杂,但是随着深入的了解,越来越喜欢了,为了更好的理解keycloak如何跟 springsecurity 解释使用,这里把 springsecurity 的功能回顾一下。

1. 核心概念

任何安全系统都包含两个概念

  • 身份验证:authentication
  • 授权:authorization

SpringSecurity 通过一系列的 Filter 来进行权限认证

1.1 过滤器

Spring 过滤器的官方文档

安全过滤器通过SecurityFilterChain API 插入到FilterChainProxy 中。过滤器的顺序很重要。通常不需要知道 Spring Security 的排序。然而,有时知道顺序是有益的FilterFilter

以下是 Spring Security Filter 排序的完整列表:

  • ChannelProcessingFilter
  • WebAsyncManagerIntegrationFilter
  • SecurityContextPersistenceFilter
  • HeaderWriterFilter
  • CorsFilter
  • CsrfFilter
  • LogoutFilter
  • OAuth2AuthorizationRequestRedirectFilter
  • Saml2WebSsoAuthenticationRequestFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • OAuth2LoginAuthenticationFilter
  • Saml2WebSsoAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • OAuth2AuthorizationCodeGrantFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

1.2 过滤器说明

怎么替换默认过滤器?

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 攻击

  • 0:禁用 XSS 保护;
  • 1:启用 XSS 保护;
  • 1; mode=block:启用 XSS 保护,并在检查到 XSS 攻击时,停止渲染页面(例如 IE8 中,检查到攻击时,整个页面会被一个#替换);

X-Frame-Options:是否允许页面被嵌套

  • DENY 表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许
  • SAMEORIGIN 表示该页面可以在相同域名页面的 frame 中展示
  • ALLOW-FROM uri 表示该页面可以在指定来源的 frame 中展示

**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 里对比看用户是否有权限

以上内容参考了这里

1.3 错误捕获

详细看官方文档

ExceptionTranslationFilter 可以将 AccessDeniedException没有权限异常AuthenticationException身份认证异常 转换成 HTTP responses.

  • AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常

    • CustomAuthenticationEntryPoint 是自己的,继承上面的类
  • AccessDeniedException 用来解决认证过的用户访问无权限资源时的异常

    • CustomAccessDeniedHandler 是自己的,继承上面的类

WebSecurityConfigurerAdapter中添加这两个错误信息

httpSecurity.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());

具体的错误处理,可以是跳转页面或者直接从 repose 中输出,这里展示跳出页面的例子。

@Log4j2
public 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类,虽然没有认证成功,也会跳转到相应的页面中。

@Bean
SecurityFilterChain web(HttpSecurity http,
AuthorizationManager<RequestAuthorizationContext> mfaAuthorizationManager) throws Exception {
MfaAuthenticationHandler mfaAuthenticationHandler = new MfaAuthenticationHandler("/second-factor");
// @formatter:off
http
.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>() {
@Override
public <O extends ExceptionTranslationFilter> O postProcess(O filter) {
filter.setAuthenticationTrustResolver(new MfaTrustResolver());
return filter;
}
})
);
// @formatter:on
return http.build();
}

1.4 授权

最常见的 Spring 可以通过权限、角色、IP 来进行授权。

① hasAuthority 权限

前面登陆的例子,将一个权限分配到了这里

//2.得到UserDetails
UserDetailsImp user = new UserDetailsImp(
dbUser.getUsername,
dbUser.getUserpassword,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal");
)

配置一个权限或者多个

.antMatchers("/main1.html").hasAuthority("admin")
多个
.antMatchers("/main1.html").hasAnyAuthority("admin","dkdks")

② hasRole 基于角色

角色必须要用 Role 开头

前面登陆的例子,将一个权限分配到了这里

//2.得到UserDetails
UserDetailsImp user = new UserDetailsImp(
dbUser.getUsername,
dbUser.getUserpassword,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc");
)
.antMatchers("/main1.html").hasRole("abc")
配置多个
.antMatchers("/main1.html").hasAnyRole("abc")

③ 基于 IP 地址

服务器端口,只能某个 IP 地址才可以调用

request.getRemoteAddr() //获取IP地址可以这么来做

.antMatchers("/main.html").hasIpAdress("127.9.9.1");

2. Jwt 认证

2.1 身份验证

做了一个拦截器 Filter,拦截 Request 中的 header,然后模拟登录。

① 注册 Filter

在自定义的WebSecurityConfigurerAdapter中添加 Filter 到用户密码验证之前。

httpSecurity.addFilterBefore(authenticationTokenFilterBean()
, UsernamePasswordAuthenticationFilter.class);

这里有一个 Spring 的基础知识:Spring 默认的 Filter 加载顺序。

② 实现 Filter

具体步骤如下:

  • 拦截每个 URL 请求
  • 获取到 Request 中的 JWT,并校验是否通过?
    • 没有通过继续执行交给 Spring 来处理权限不通过的内容。
  • 通过后从 JWT 中解析出 UserName,并到数据库中查询出用户的信息与权限。
  • 然后模拟出一个AuthenticationToken,这里会默认设置成已经登录setAuthenticated(true)
    • 就是因为authenticated==true,后续默认的 Spring 关于权限验证 Filter 才会放行。
  • AuthenticationToken 放入SecurityContextHolder

2.2 授权

这里采用了一种资源授权的模式,将所有的 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();
}
});
}

② 分配授权

  • 用户登录后得到角色
  • 然后根据角色得到资源权限
  • 然后在 Filter 把权限添加到这个登录用户Authentication

下面代码是在CustomUserDetailsService中初始化 Authentication 的具体实现类UserDetails

@Override
public 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;
}
}