一 理论优先
(一) 重大角色认识
第一要知道这么几个事情,实战中会使用到哦~
- SecurityContext: Spring Security 的上下文对象, Authentication 认证对象会放在里面,若用户未认证,则 Authentication 。对象的内容为 null
- SecurityContextHolder:用于拿到上下文对象的静态工具类,使用方法 SecurityContextHolder.getContext() 就可以获取到 SecurityContext 对象,
- Authentication:认证接口,定义了认证对象的数据形式,列如用户名密码
- AuthenticationManager:认证管理器。这个接口规范了 Spring Security 的过滤器要如何执行身份认证,并在身份认证成功后返回一个经过认证的 Authentication 对象
(二)Spring Security的过滤器链
之前我们介绍 Spring Security 的时候,曾经提到过 Spring Security 采用的是责任链的设计模式,它有一条很长的过滤器链来实现各个环节的控制逻辑

preview
如上图,每个 WEB 请求,会经过这么一条过滤器链,在经过过滤器的过程中会完成认证与授权。如果中间发现这个请求未认证或者未授权,就会抛出异常
Spring Security 的过滤器链有十几个,我们本篇主要讲的是 Spring Security 采用 JWT 实现认证方案,所以只关注以下几个:
- SecurityContextPersistenceFilter ,在新版本中已经被废弃,推荐使用 SecurityContextHolderFilter 。在运行应用程序的其余部分之前, SecurityContextHolderFilter 从 SecurityContextRepository 加载 SecurityContext 并将其设置在 SecurityContextHolder 上
- UsernamePasswordAuthenticationFilter 是针对使用用户名和密码进行身份 认证 而定制化的一个过滤器,如果将原本默认的认证方案变更为 JWT ,那么就需要定义一个过滤器,并配置在其前面 将认证信息存储至上下文 之后,再进行认证
- FilterSecurityInterceptor 会根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限,从而决定是否允许访问
二 实战
既然 JWT 只是不同于 Session 的另一种认证方案,那么,我们的实战,也将在 Spring Security demo 的基础上,将原本的 Session 机制,更换为 JWT 而已
(一)Spring Security配置类
Spring Security 配置类 – SecurityConfig 如下

在 Spring Security demo 的基础上,为了将认证方案更改为 JWT ,做了两个地方的改动
1 认证过滤器配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry
authorizeRequests = http.csrf().disable().authorizeRequests();
// 1.查询到所有的权限
List<Permission> allPermission = permissionMapper.findAllPermission();
// 2.分别添加权限规则
allPermission.forEach((p -> {
authorizeRequests.antMatchers(p.getUrl()).hasAnyAuthority(p.getName()) ;
}));
authorizeRequests.and()
// 配置为 Spring Security 不创建使用 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/**").fullyAuthenticated()
//不用验证登录接口
.antMatchers("/authenticate").permitAll()
.anyRequest().authenticated();
//配置认证过滤器 jwtAuthenticationFilter
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
复制代码
这里实则在之前 Spring Security 项目的基础上,增加了以下两项的配置
1)配置为 Spring Security 不创建使用 session
2)配置认证过滤器 jwtAuthenticationFilter
可以顺便复习一下:
SecurityFilterChain Bean 实际上是在新版本( 2.7.0 版本) Spring Security 中,用来定义各个资源能够被拥有哪些权限的用户所访问的规则
如资源 /user/common 可以分别被具有 admin 、 common 权限的用户所访问,而 /user/admin 则只可以被具有 admin 权限的用户所访问
2 配置认证接口的放行规则
/**
* 资源放行配置
* @return
*/
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring().antMatchers("/hello");
web.ignoring().antMatchers("/login");
//登录接口放行
web.ignoring().antMatchers("/authenticate");
web.ignoring().antMatchers("/css/**", "/js/**");
};
}
复制代码
登录接口 /authenticate 是用来获取 token 的,它本身当然无法携带认证信息,所以这里配置我们后续所定义的登录接口 /authenticate 不用被校验
(二)JWT 认证过滤器
用于每次请求的拦截处理。 验证 token ,并将验证之后的 token 对应的用户信息 存储至上下文 中
/**
* JWT 认证过滤器
*/
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
//1-获取 token
String token = request.getHeader("Authorization");
if (StrUtil.isBlank(token)) {
//放行,会自动执行后面的过滤器
logger.info("请求头不含 JWT token 或者 token 的值为空,调用下个过滤器");
filterChain.doFilter(request,response);
return;
}
//2-获取认证信息
Authentication authentication = jwtService.getAuthentication(token);
//3-设置用户验证对象至上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException e) {
SecurityContextHolder.getContext().setAuthentication(null);
} finally{
filterChain.doFilter(request, response);
}
}
}
复制代码
这里通过继承 OncePerRequestFilter 类,对于客户端的请求进行拦截处理,并确保在一次请求中只通过一次过滤,避免重复执行
过滤器中的逻辑为:
1 获取 token。 第一从客户端的请求头中 获取 token 的值 ,并做 非空校验
2 获取认证对象。 调用JwtService的getAuthentication()方法 得到Authentication对象
进入 JwtService 的 getAuthentication() 方法
/**
* 获取认证过的token对应的信息,包括用户以及用户对应的权限
* @param token
* @return
*/
public Authentication getAuthentication(String token) {
// 1-根据 token 和秘钥,解析出 JWT 的 claims 对象
Claims claims =
Jwts.parser()
.setSigningKey(KEY)
.parseClaimsJws(token)
.getBody();
// 2-获取权限信息,并转换为集合类型
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
//3-得到用户对象 principal
User principal = new User(claims.getSubject(), "", authorities);
//4-得到认证 token 对象(UsernamePasswordAuthenticationToken 实现了 Authentication 接口,表明是通过用户名密码认证过的 token 认证信息)
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
复制代码
1)得到 Claims 对象
根据 token 和秘钥,解析出 Claims 对象,这个里面就存储了我们的 JWT 的 payload 中各个参数值。 debug 可以看到,这个里面实际上是我们在创建 token 时,存进去的数据:用户的用户名 sub 、权限列表 auth 以及过期时间 exp

2)得到权限列表 authorities 。 获取权限信息,并转换为集合类型(生成 token 时,是以字符串的形式存储的)
3)将认证过后的这个用户对象配置到 Security 上下文:SecurityContextHolder
后续的 FilterSecurityInterceptor 会根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限,从而决定是否允许访问,请求结束之后会清除掉该用户信息
一般利用 ThreadLocal 机制来保存每个使用者的 SecurityContext ,就避免了多线程并发导致信息错乱等问题了
(三)登录接口-生成token
@RestController
@CrossOrigin
public class JwtAuthenticationController {
@Resource
private UserService<User> userService;
@Resource
private JwtService jwtService;
/**
* 登录接口 - 用于生成 token
* @param username 用户名
* @param password 密码
* @return
* @throws Exception
*/
@PostMapping("/authenticate")
public ResponseResult login(String username, String password) throws Exception {
//1-校验用户名密码是否为空
if(StrUtil.isBlank(username) || StrUtil.isBlank(password)){
throw new Exception("用户名或密码不能为空!");
}
// 2-根据用户查询用户是否存在
User user = userService.findByUsername(username);
if (user == null){
throw new Exception("用户名或密码有误!");
}
//3-验证用户名密码
password = MD5Util.md5slat(password);
if (!password.equalsIgnoreCase(user.getPassword())){
throw new Exception("用户名或密码有误!");
}
UserVo userVo = UserVo.builder().build();
userVo.setId(user.getId());
userVo.setUsername(username);
userVo.setPassword(password);
//4- 生成 token
String token = jwtService.createToken(userVo);
userVo.setToken(token);
userVo.setRefreshToken(UUID.randomUUID().toString());
return new ResponseResult(userVo);
}
}
复制代码
这里,就与上一篇中的登录接口的逻辑一样了,也是以下四个步骤:
1 校验用户名密码是否为空
2 根据用户名查询用户是否存在
3 验证用户名密码是否正确。(需要提前通过 MD5Util.md5slat(password ) 方法得到加密之后的密码,写入表 User 中的密码)
列如,我的密码是 123456 ,第一采用 MD5Util.md5slat(123456) 生成一个秘钥

运行 main 方法,输出的秘钥为:
53b432e61314e00bcc99287af9537dc4
填写在 user 表中

**4 生成 token 。**调用 jwtService.createToken() 方法生成 token ,并返回 UserVo 对象
那么, token 是怎样生成的呢?
进入 jwtService.createToken() 方法
/**
* 创建 token
* @param userVo 用户对象
* @return token
*/
public String createToken(UserVo userVo) {
String authorities = userVo.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
return Jwts.builder()
.setSubject(userVo.getUsername())
.claim(AUTHORITIES_KEY, authorities)
.setExpiration(new Date(now + TOKEN_EXSPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
复制代码
采用 Jwts.builder() 创建一个 JwtBuilder 构建器,然后分别配置用户的用户名 sub 、权限列表 auth 以及过期时间 exp ,指定加密算法,调用 compact() 方法得到一个字符串,即生成了一个 token 字符串
(四)资源接口
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/common")
public String common() {
return "hello~ common";
}
@GetMapping("/admin")
public String admin() {
return "hello~ admin";
}
}
复制代码
分别定义 /user/common 和 /user/admin 两个 API ,作为用户的权限,用于测试不同用户的权限
这里只列出了该项目的主要类,此外,还分别需要创建用于密码加密的 MD5Util 工具类, User 、 UserVo 、 ResponseResult 等实体类, 用于项目本身的功能完善
三 测试
需要说明一下,本次的项目是在 Spring Security 文章里面的 demo(** github.com/helemile/Sp… Session 变更为 JWT 认证方案而已,原先的实现逻辑完全没有变化
所以,RBAC 权限控制表: user , role , user_role , permission , role_permission 表中的信息完全一样(除了密码采用了 MD5 加盐的方式加密以外)
即:共存在两个用户: admin 和 user ,他们所对应的权限列表分别为:
user 用户: common 权限,可以访问的资源列表: /user/common
admin 用户: admin 权限,可以访问的资源列表: /user/admin , /user/common
(一)user 用户
1 调用登录接口,获取token(user用户)
使用 user 账号进行登录,该账号只有 common 权限

登录之后,成功返回当前用户的信息,包括用户名,密码, token 以及权限列表 authorities
2 不携带token,直接访问资源/user/common

接口返回 403 -禁止访问
3 携带token,访问资源/user/common

访问成功
4 携带token,访问资源/user/admin
当然,由于 user 用户只有 common 权限,所以如果访问 /user/admin 资源,也会返回 403

若换成 admin 用户登录呢?
(二)admin 用户
1 调用登录接口,获取token(admin用户)

2 访问/user/admin资源

成功访问了
也就是说,我们的目的就达到了:成功将 Spring Security 的认证方案替换成了 JWT ,实现了 Sprinfg Security 与 JWT 的结合
四 总结
今天我们讲了:
1 Spring Security 的几个重大类或接口: SecurityContext 、 SecurityContextHolder“Authentication 以及 AuthenticationManager
2 Spring Security 的过滤器链说明
3实战实现 Spring Security 与 JWT 认证方案的集成,共同实现了分布式系统的认证功能
关于用户的权限列表 authorities 如何获取这个问题,网上的许多实战例子是 在每次过滤器解析时,根据用户名去数据库查询一次,得到权限列表 。这样的效果实则跟 Session 差不多了,由于同样每次请求都要请求一次持久层呢~
我们这里实战 demo 的思路是将 用户以及用户的权限列表 authorities 作为生成 token 的其中一个数据项,从而在过滤器中解析完 token 即可获得,从而存储至认证对象: Authentication 中,而不用专门从数据库中获取一次,这种设计还是相对比较巧妙的














- 最新
- 最热
只看作者