用户认证流程

  1. 客户端第一次发送请求,由于此前未登录认证,因此会被spring security过滤器拦截;若此次请求为登录请求,由于在SpringSecurity配置类声明的白名单(即不需要被认证的请求)里配置了登录路径,因此可以被接收。
  2. 客户端接收登录请求后到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);

图解初识认证流程:

springboot 接口白名单的文件 spring security 白名单规则_java

  • 返回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到客户端。

  1. 客户端(以浏览器为例)接收到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);