前言
主要实现 Spring Security
的安全认证,结合 RESTful API
的风格,使用无状态的环境。
主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个认证的方法,然后进行认证,授权成功返回授权实例信息,供服务调用。
基于Token的身份验证的过程如下:
- 用户通过用户名和密码发送请求。
- 程序验证。
- 程序返回一个签名的token 给客户端。
- 客户端储存token,并且每次用于每次发送请求。
- 服务端验证token并返回数据。
每一次请求都需要token,所以每次请求都会去验证用户身份,所以这里必须要使用缓存,
基本使用
加入相关依赖
1 | <dependency> |
了解基础配置
认证的基本信息
1 | public interface UserDetails extends Serializable { |
获取基本信息
1 | // 根据用户名查找用户的信息 |
我们只要实现这个扩展,就能够自定义方式获取认证的基本信息
WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
提供了一种便利的方式去创建 WebSecurityConfigurer
的实例,只需要重写 WebSecurityConfigurerAdapter
的方法,即可配置拦截什么URL、设置什么权限等安全控制。
下面是主要会是要到的几个配置:
1 | /** |
认证流程
阅读源码了解。
AbstractAuthenticationProcessingFilter.doFilter
1 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) |
UsernamePasswordAuthenticationFilter.attemptAuthentication
1 | // 接收并解析用户登陆信息,为已验证的用户返回一个已填充的身份验证令牌,表示成功的身份验证, |
ProviderManager.authenticate
验证 Authentication 对象(里面包含着验证对象)
- 如果有多个 AuthenticationProvider 支持验证传递过来的Authentication 对象,那么由第一个来确定结果,覆盖早期支持AuthenticationProviders 所引发的任何可能的AuthenticationException。 成功验证后,将不会尝试后续的AuthenticationProvider。
- 如果最后所有的 AuthenticationProviders 都没有成功验证 Authentication 对象,将抛出 AuthenticationException。
最后它调用的是 Authentication result = provider.authenticate(authentication);
只要我们自定义 AuthenticationProvider
就能完成自定义认证。
动手实现安全框架
使用的依赖
1 | <dependencies> |
数据表关系
User
1 |
|
Role
1 |
|
UserRole
1 |
|
流程实现
认证流程:
JWT
我使用的是服务端无状态的token 交换的形式,所以引用的是 jwt,首先实现 jwt:
1 | # jwt 配置 |
实现 UserDetails 和 UserDetailsService
实现 UserDetails
1 | public class UserDetailsImpl implements UserDetails { |
实现 UserDetailsService
1 | 4j |
SecurityModelFactory
转换 UserDetails 的工厂类
1 | public class SecurityModelFactory { |
授权认证
登陆过滤器
1 | public class LoginFilter extends UsernamePasswordAuthenticationFilter { |
- 首先会进入
doFilter
方法中,这里可以自定义定义过滤; - 然后如果是
登陆
的请求,会进入attemptAuthentication
组装登陆信息,并且进行登陆认证; - 如果登陆成功,会调用
successfulAuthentication
方法。
登陆验证
1 | 4j |
我们自己定义的 AuthenticationProvider
主要是实现前面经过过滤器封装的认证对象 UsernamePasswordAuthenticationToken
进行解析认证,
如果认证成功 就给改 UsernamePasswordAuthenticationToken
设置对应的权限,然后返回 Authentication
- 获得认证的信息;
- 去数据库查询信息,获取密码解密验证认证信息;
- 认证成功,设置权限信息,返回
Authentication
,失败抛出异常。
JWT 拦截器
1 | /** |
- 请求进入
doFilterInternal
方法中,对请求是否带token
进行判断, - 如果没有token,则直接放行请求;
- 如果有 token,则解析它的 post;
配置权限和相关设置
自定义配置 Spring Security 配置类 WebSecurityConfig
,进项相关配置,并且将所需要的类注入到系统中。
1 |
|
权限控制
1 |
|
请求上面的getAllUser
方法,需要当前用户同时拥有 ROLE_USER
和 ROLE_admin
两个权限,才能通过权限验证。
在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:比如 ‘hasRole()’ 来决定哪些用户有权访问。需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 ‘ROLE_’。
迭代上个版本
后来,我发现进行用户认证的时候,会将所有的 provider 都尝试一遍,那么外面将登陆的 UsernameAndPasswordToken
和 JwtTToken
都可以分别进行验证进行了啊,所有我预先定义 UsernamePasswordAuthenticationToken
包装登陆的信息,然后进入登陆的 AuthenticationProvider
进行认证,token
验证形式,使用 PreAuthenticatedAuthenticationToken
的包装,然后进入例外一个 AuthenticationProvider
中认证。
现在我们的流程就更加清晰了。
所以现在我对以前的权限配置以及认证进行了一些更改:
过滤器
在这里,我根据不同请求的类型,进行不同的适配,然后进行加工分装成不同的认证凭证,然后根据凭证的不同,进行不同的认证。
1 | 4j |
授权认证
根据提供的凭证的类型,进行相关的验证操作
LoginAuthenticationProvider
跟上个版本的 登陆验证中的 CustomAuthenticationProvider
代码一样实现一样。
TokenAuthenticateProvider
根据 token 查找它的 权限 信息,并装在到认证的凭证中。
1 | public class TokenAuthenticateProvider implements AuthenticationProvider { |
配置权限和相关设置
和上个版本没什么变化,只是将类换了一下
1 |
|
后续完善
- 修改密码,登出操作 token 的失效机制;
- OAuth2 授权服务器的搭建;
- 修改权限后,下次请求刷新权限;
- ……
附录一:HttpSecurity常用方法
方法 | 说明 |
---|---|
openidLogin() | 用于基于 OpenId 的验证 |
headers() | 将安全标头添加到响应 |
cors() | 配置跨域资源共享( CORS ) |
sessionManagement() | 允许配置会话管理 |
portMapper() | 允许配置一个PortMapper (HttpSecurity#(getSharedObject(class)) ),其他提供SecurityConfigurer 的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl 映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() | 配置基于x509的认证 |
rememberMe | 允许配置“记住我”的验证 |
authorizeRequests() | 允许基于使用HttpServletRequest 限制访问 |
requestCache() | 允许配置请求缓存 |
exceptionHandling() | 允许配置错误处理 |
securityContext() | 在HttpServletRequests 之间的SecurityContextHolder 上设置SecurityContext 的管理。 当使用WebSecurityConfigurerAdapter 时,这将自动应用 |
servletApi() | 将HttpServletRequest 方法与在其上找到的值集成到SecurityContext 中。 当使用WebSecurityConfigurerAdapter 时,这将自动应用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter 时,默认启用 |
logout() | 添加退出登录支持。当使用WebSecurityConfigurerAdapter 时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe() 身份验证,清除SecurityContextHolder ,然后重定向到”/login?success” |
anonymous() | 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter 结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken 表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String) ,则将生成默认登录页面 |
oauth2Login() | 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() | 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
httpBasic() | 配置 Http Basic 验证 |
addFilterAt() | 在指定的Filter类的位置添加过滤器 |