用户认证流程
-
客户端第一次发送请求,由于此前未登录认证,因此会被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);
-