用户认证流程
- 客户端第一次发送请求,由于此前未登录认证,因此会被spring security过滤器拦截;若此次请求为登录请求,由于在SpringSecurity配置类声明的白名单(即不需要被认证的请求)里配置了登录路径,因此可以被接收。
- 客户端接收登录请求后到service层处理登录业务,此请求参数DTO携带用户名和密码信息,然后把其声明在认证信息里面:
Authentication authentication//声明一个认证信息
= new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword());
接下来用认证管理器对认证信息进行处理:
Authentication authenticateResult //调用认证管理器对认证信息进行处理
= authenticationManager.authenticate(authentication);//并获取认证信息把他放在上下文里面
此认证过程实质上主要是调用了UserDetailsService里面被重写的loadUserByUsername(参数为用户名)方法进行查询数据库中是否存在此用户的处理,若无(即登录的用户不存在)则抛出异常,若存在则返回UserDetails对象:
- 根据用户名查询出用户对象
- 获取用权限并把权限放入GrantedAuthority里面
List<GrantedAuthority> authorities = new ArrayList<>();
//把权限放入GrantedAuthority里面
for (String permission : admin.getPermissions()){
GrantedAuthority authority=new SimpleGrantedAuthority(permission);
authorities.add(authority);
}
- 自定义的含有id的认证结果当事人对象
AdminDetails adminDetails = new AdminDetails(
admin.getId(),
admin.getUsername(),
admin.getPassword(),
admin.getEnable() == 1,
authorities);
图解初识认证流程:
- 返回adminDetails对象(AdminDetails extends User,User implements UserDetails)
返回的adminDetails对象实质上就是authenticateResult认证结果里面的当事人信息(认证结果包含Principal当事人、Credentials凭证、Authorities权限清单;其他俩是框架生成的);
//从认证结果里得到adminDetails当事人信息
Object principal = authenticateResult.getPrincipal();
AdminDetails adminDetails = (AdminDetails) principal;
得到当事人信息接下来就需要生成jwt令牌:
- 获取其用户名、id、权限(需要传换成json字符串)
- jwt包含三大部分:头部信息Header、载荷Payload、签名Signature,其中载荷存放用户有效信息
- 把有效信息存放到载荷里:
Map<String, Object> claims = new HashMap<>();
// claims.put();放入到jwt载荷有效信息里面
claims.put("username", username);
claims.put("id",id);
claims.put("authoritiesJsonString", authoritiesJsonString);
- 生成jwt
String jwt = Jwts.builder()
// Header
.setHeaderParam("alg", "HS256")
.setHeaderParam("typ", "JWT")
// Payload
.setClaims(claims)
// Signature
.setExpiration(date)
//生成签名时需要的secretKey,再被解析时会用到
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
登录业务处理完毕响应jwt到客户端。
- 客户端(以浏览器为例)接收到jwt后存放到localstorage中,此后发送请求都要携带jwt。当请求发送到服务器时,会先经过框架的过滤器组件(因为组件类会在项目启动时就被实例化到容器中以备框架随时调用),在我们自定义的过滤器中对jwt进行过滤。
(过滤器需要做的: 解析JWT、创建认证对象、将认证对象存入到SecurityContext上下文):
- 获取请求携带的jwt并判断其是否有效
String jwt = request.getHeader("Authorization");
//StringUtils.hasText()调用String工具类判断字符串是否为null或empty
if (!StringUtils.hasText(jwt) || jwt.length() < JWT_MIN_LENGTH) {
// 对于无效的JWT,直接放行,交由后续的组件进行处理
log.debug("获取到的JWT被视为无效,当前过滤器将放行……");
filterChain.doFilter(request, response);
return;
}
- 解析获取到的有效的jwt有效信息(载荷)
claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
- 在解析时可能会出现各种异常:ExpiredJwtException、MalformedJwtException、SignatureException、Throwable等,需要对其catch处理,例:
catch (ExpiredJwtException e) {
log.debug("解析JWT时出现ExpiredJwtException");
String message = "登录信息已过期,请重新登录!";
JsonResult<Void> jsonResult = JsonResult.fail(ServiceCode.ERR_JWT_Expired, message);
String jsonResultString = JSON.toJSONString(jsonResult);
PrintWriter writer = response.getWriter();
writer.println(jsonResultString);
return;
}
- 获取jwt中的用户信息(id、用户名、权限)
String username = claims.get("username", String.class);
Long id = claims.get("id",Long.class);
String authoritiesJsonString=claims.get("authoritiesJsonString",String.class);
- 接下来需要创建认证对象
LoginPincipal loginPincipal = new LoginPincipal(id,username);
Authentication authentication
= new UsernamePasswordAuthenticationToken(
loginPincipal, null, authorities);
- 再将认证对象Authentication存入到SecurityContext上下文里,之后交给框架自动去查
SecurityContextHolder.getContext().setAuthentication(authentication);
- 过滤器链继续向后传递,放行
filterChain.doFilter(request,response);
- 请求接下来到spring security框架自己的过滤器里,框架会检查上下文里面是否存在有效当事人信息,有的话放行,请求正常访问控制器,无的话拦截请求(若此次请求不携带jwt,仍有可能通过认证,原因是此前有携带有效jwt的请求通过后,使得security上下文里面存在之前的缓存)。
为实现以上操作,要让咱自定义的过滤器在框架之前被执行,需要把jwt过滤器添加到spring security框架的过滤器链中,并且在其之前:
http.addFilterBefore(
jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);