JWT介绍和Spring Security的小例子都有了,接下来,我们来编写一个使用JWT作为验证方式的Spring Security的小程序。
本文是上一篇Spring Security小例子的扩展,区别在于:
- 使用JWT token
- 使用MySQL数据库存储信息
代码
本文的具体源代码请参考 https://github.com/dukeding/spring-boot-security-jwt_0116
注意事项
- 该程序使用了MySQL数据库,所以请先在运行环境中安装并配置好MySQL。注意,在
application.properties
文件里配置了数据源,请确保端口,DB,用户名,密码都设置正确。 - 不需要在DB中手工创建table,运行程序时会自动创建
USERS
,ROLES
等table。但要注意,需要手工在ROLES
table中插入几条记录:
INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
程序功能
- 用户可以注册新账号,或者使用username和password登录
- 用户通过其角色(admin,moderator,user)来被授权访问资源
具体API如下:
Method | URI | 说明 |
---|---|---|
POST | /api/auth/signup | 注册新账号 |
POST | /api/auth/signin | 登录 |
GET | /api/test/all | 获取公开信息(无需登录) |
GET | /api/test/user | 获取User信息 |
GET | /api/test/mod | 获取Moderator信息 |
GET | /api/test/admin | 获取Admin信息 |
程序架构
从图中可见,从左到右,该程序可分为3层:
- HTTP
- Spring Security
- REST API
其中重点是 Spring Security
层,也就是蓝框所框起来的部分。它位于HTTP和REST API之间,所负责的事情包括:
- 接收HTTP请求
- 过滤
- 验证(authenticate)
- 存储Authentication数据
- 生成token
- 获取User详细信息
- 授权(authorize)
- 处理异常
- ……
其中一些术语简介如下:
SecurityContextHolder
提供对SecurityContext的访问SecurityContext
持有Authentication以及可能的与request有关的安全信息Authentication
代表那些包含“反映了授予principal的application级别的permission的GrantedAuthority”的principalUserDetails
包含了从DAO或者其它安全数据构建Authentication对象所需要的信息UserDetailsService
协助从基于字符串的username创建UserDetails,通常被AuthenticationProvider使用。UserDetailsService通过Spring Data JPA与MySQL数据库连接JwtAuthTokenFilter
(扩展了OncePerRequestFilter)预处理HTTP请求,从token,创建Authentication并将其注入到SecurityContextJwtProvider
校验,解析token字符串,或者从UserDetails生成token字符串UsernamePasswordAuthenticationToken
从登录请求获取username/password,并结合到Authentication 接口的实例中AuthenticationManager
使用DaoAuthenticationProvider(由UserDetailsService和PasswordEncoder协助)来校验UsernamePasswordAuthenticationToken的实例,然后对于成功的身份验证,返回一个填充好的Authentication实例SecurityContext
调用SecurityContextHolder.getContext().setAuthentication(…)(以及上面所返回的Authentication对象)所搭建AuthenticationEntryPoint
处理AuthenticationException- 对Restful API的访问受保护于HTTPSecurity,并由Method Security Expression所授权
流程
下图显示了用户注册,登录,授权的流程:
下图显示了更新token的流程:
参考
- https://www.bezkoder.com/spring-boot-jwt-authentication
- https://www.bezkoder.com/spring-boot-jwt-mysql-spring-security-architecture
- https://github.com/bezkoder/spring-boot-spring-security-jwt-authentication
附(流程的细化,未完)
接收HTTP请求
当一个HTTP请求到来时(无论从浏览器,web service客户端,HttpInvoker或者一个AJAX程序,Spring并不关心),它会经由一个filter链来做验证和授权。
过滤请求
我们添加了JwtAuthTokenFilter(扩展了Spring的OncePerRequestFilter抽象类)到filter链里。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
...
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
JwtAuthTokenFilter使用JwtProvider来校验token:
class JwtAuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtProvider tokenProvider;
@Override
protected void doFilterInternal(...) {
String jwt = getJwt(request);
if (jwt!=null && tokenProvider.validateJwtToken(jwt)) {
...
}
filterChain.doFilter(request, response);
}
}
现在分两种情况:
- Login/SignUp:非保护API -> 用AuthenticationManager来验证Login请求,如果发生错误,用AuthenticationEntryPoint来处理AuthenticationException。
- 受保护的资源:
- JWT token为空/无效 -> 如果出现验证错误,用AuthenticationEntryPoint来处理AuthenticationException。
- JWT token有效 -> 从token获取User信息,然后创建AuthenticationToken。
从token创建AuthenticationToken
JwtAuthTokenFilter使用JwtProvider从接收到的token解析出username/password,然后基于解析出来的数据,JwtAuthTokenFilter将会:
- 创建一个AuthenticationToken(实现了Authentication)
- 将该AuthenticationToken作为Authentication对象,并将其存储在SecurityContext,为将来的filter所用(例如Authorization filter)。
// extract user information
String username = tokenProvider.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// create AuthenticationToken
UsernamePasswordAuthenticationToken authentication
= new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));