环境:Java8+SpringBoot 2.3.8+Spring Security 5.3.6 + java-jwt 3.12.0
主要是利用过滤器拦截(Filter)
实现了连接数据库,注册登录用户,使用Spring Security结合JWT实现安全认证。
前后端分离,使用json提交数据
Spring Security
Spring Security是一个功能强大、高度可定制的,并且专注于向Java应用提供**身份验证(authentication )和授权(authorization)**的框架。
authentication与authorization
authentication主要是确定你是谁,而authorization是可以赋予你访问资源,做某些事的权利。
JWT(JSON Web Token)
JWT主要是用来确定用户的身份信息。在用户第一次携带用户名和密码访问服务器时,服务器签发一个token给用户。接下来用户携带着这个token访问服务器,就不再需要用户密码再次登录。
具体可参考该博客
什么是 JWT – JSON WEB TOKEN
表数据结构
本次用例采用MongoDB,这里的数据库实现的并不是很重要,大家可以随意:)
tb_user
tb_role
tb_resource
tb_role_res
SpringBoot
新建一个SpringBoot项目
本项目使用了lombok插件
@Data
主要用于生成getter和setter
@AllArgsConstructor
生成全参构造函数
@NoArgsConstructor
生成无参构造函数
@Document(value = "tb_user")
关联MongoDB的表
@Id
声明表的主键
- 如果你不使用lombok插件,按照往常写法即可
- 与表的关联等操作,按照对应数据库的方法操作
有关的Json操作使用了fastjson
maven
1 | xml复制代码<dependency> |
pom.xml
完整的pom.xml文件
1 | xml复制代码<?xml version="1.0" encoding="UTF-8"?> |
entity
UserDo.java
1 | java复制代码package com.example.demo.entity; |
RoleDo.java
1 | java复制代码package com.example.demo.entity; |
ResourceDo.java
1 | java复制代码package com.example.demo.entity; |
RoleResourceDo.java
1 | java复制代码package com.example.demo.entity; |
dao
UserDao.java
1 | java复制代码package com.example.demo.dao; |
service
UserService.java
1 | java复制代码package com.example.demo.service; |
impl
UserServiceImpl.java
1 | java复制代码package com.example.demo.service.impl; |
controller
UserController.java
1 | java复制代码package com.example.demo.controller; |
Spring Security
Spring Security是通过一系列的Filter(过滤器)来实现它的功能的
Filter的顺序与作用
以下是默认的Filter顺序
在debug模式下可以看到过滤器执行的顺序
其中3,4是我自定义的过滤器,以下几种方法是用来添加过滤器的
1 | java复制代码public HttpSecurity addFilterAfter(Filter filter, Class<? extends Filter> afterFilter); //在某个过滤器后添加一个过滤器 |
filters
- ChannelProcessingFilter
- SecurityContextPersistenceFilter
- LogoutFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- UsernamePasswordAuthenticationFilter
- OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- ConcurrentSessionFilter
- DigestAuthenticationFilter
- BearerTokenAuthenticationFilter
- BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
- SwitchUserFilter
Spring Security Web 5.1.2 源码解析 – 安全相关Filter清单
认证流程
以上两张图很清晰地展现了Spring Security的主要认证流程。
在接收到请求后,先经过一系列的过滤器。
当被UsernamePasswordAuthenticationFilter
拦截到后,调用其attemptAuthentication(request,response)
方法,获取到username和password后,封装成UsernamePasswordAuthenticationToken
。
1 | java复制代码UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password); |
再让AuthenticationManager
调用authenticate(authentication)
方法进行验证。
1 | java复制代码return this.getAuthenticationManager().authenticate(authenticationToken); |
AuthenticationManager
的默认实现是ProviderManager
,其内部维护着一个AuthenticationProvider
的列表。这个列表中存放的就是就是各种认证方式。当调用ProviderManager
的authenticate(authentication)
方法时,会遍历该列表。
当认证成功时,会返回一个经过身份验证的对象Authentication
,并且不会执行后面的认证方法。若全部验证失败,则会抛出AuthenticationException
异常。
1 | java复制代码//部分ProviderManager的authenticate(Authentication authentication)方法源代码 |
AuthenticationProvider
的一个实现AbstractUserDetailsAuthenticationProvider
会响应传过来的UsernamePasswordAuthenticationToken
身份验证请求。
AbstractUserDetailsAuthenticationProvider
调用authenticate(authentication)
方法,根据传过来的Authentication
获取用户名后,从缓存或者调用retrieveUser(username, authentication)
方法。该方法由AbstractUserDetailsAuthenticationProvider
的子类DaoAuthenticationProvider
实现,主要是调用了UserDetailsService
的loadUserByUsername(username)
方法加载用户信息。我们一般会重写该方法,从数据库中取出用户信息。
在获取了正确的用户信息UserDetails
和根据前面传数据过来后封装的UsernamePasswordAuthenticationToken
后,调用DaoAuthenticationProvider
实现的additionalAuthenticationChecks(userDetails,authentication)
比较两者的密码是否一致完成验证。
1 | java复制代码//retrieveUser方法源码 |
如图是AuthenticationManager
和AuthenticationProvider
等几个类的关系
Json提交数据登录
因为Spring Security是默认通过form表单提交数据进行登录验证的。所以要通过URL访问后台并提交Json数据,需要修改Spring Security的配置。
在查阅资料的过程中,我发现在改写登录身份验证时,有的是从UsernamePasswordAuthenticationFilter
继承,有的是从AbstractAuthenticationProcessingFilter
继承。
UsernamePasswordAuthenticationFilter和AbstractAuthenticationProcessingFilter的区别
默认是使用UsernamePasswordAuthenticationFilter
来拦截表单登录请求的,而UsernamePasswordAuthenticationFilter
是从AbstractAuthenticationProcessingFilter
继承而来的。
UsernamePasswordAuthenticationFilter
是用来处理表单登录的,默认登录URL为/login
;需要提供两个参数:用户名和密码,也有默认的参数名,分别为username
和password
。要修改它的认证方法,主要是通过重写 attemptAuthentication(request,response)
方法。
1 | java复制代码public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response); |
若是使用表单登录,可以在继承WebSecurityConfigurerAdapter
的类里修改
1 | java复制代码@Configuration |
若是使用Json传递数据登录,可以参考里的JwtLoginFilter
AbstractAuthenticationProcessingFilter
是基于浏览器HTTP的身份验证请求处理器
继承该类时,主要需要设置三个地方
- 设置authenticationManager属性,是用来处理身份认证请求的token
- 需要设置RequestMatcher设置拦截登录用的URL
- 实现
attemptAuthentication()
方法
AuthenticationSuccessHandler
当验证成功时,会调用该Handler,默认实现是SavedRequestAwareAuthenticationSuccessHandler
。它将用户重定向到ExceptionTranslationFilter
中设置的DefaultSavedRequest
,否则会重定向到Web应用程序的根目录。
也可以在验证成功后,重写successfulAuthentication()
方法,调用顺序是先执行successfulAuthentication()
,再执行Handler。要注意的是,如果重写successfulAuthentication()
时,没有调用chain.doFilter(request, response)
,则不会再调用Handler
AuthenticationFailureHandler
默认实现是SimpleUrlAuthenticationFailureHandler
。它向客户端发送401错误代码,也可以配置失败的URL
1 | java复制代码package com.example.demo.filter; |
从数据库加载用户信息
上面提到过,DaoAuthenticationProvider
的retrieveUser(username, authentication)
方法中调用了UserDetailsService
的loadUserByUsername(username)
方法。所以我们继承UserDetailsService
接口,实现该方法。
1 | java复制代码package com.example.demo.service.impl; |
JWT
JWT官网里面有各种语言的JWT实现,这里选择的是auth0的
maven
1 | xml复制代码<dependency> |
JWT工具类
1 | java复制代码package com.example.demo.util; |
添加JWT Token
在通过身份验证后,我们为利用用户的用户名和私钥生成一个Jwt token返回给前台。
新建一个JsonLoginSuccessHandler
类来处理该逻辑
1 | java复制代码package com.example.demo.handler; |
配置Filter
配置一个登录失败的处理器
1 | java复制代码package com.example.demo.handler; |
配置Filter
1 | java复制代码package com.example.demo.config; |
验证Jwt Token
1 | java复制代码package com.example.demo.filter; |
1 | java复制代码package com.example.demo.config; |
Security配置
1 | java复制代码package com.example.demo.config; |
通用类
1 | java复制代码package com.example.demo.common; |
1 | java复制代码package com.example.demo.common; |
1 | java复制代码package com.example.demo.util; |
测试
使用Postman进行测试
使用管理员的账号密码进行登录,可以看到返回的header中带有Authorization
下一次请求带上该token,成功获取数据
相同的操作,更换成普通用户登录。
获取token后访问只有管理员能访问的接口,可以看到返回了403没有权限
如果不带token访问,会返回401没有授权
小结
本文主要描述了使用如何使用Spring Security和JWT结合Springboot进行登录验证。实现这个功能,还有多种配置方式。可以使用Form表单登录,前后端结合在一起。还有更多个性化的配置,如不使用UsernamePasswordAuthenticationToken
,新建一个Token类;还有使用多种验证方式,邮箱登录等。
参考
SpringSecurity 核心组件介绍 + 认证流程 +内置拦截器顺序
本文转载自: 掘金