如何使用SpringSecurity保护你的REST(3)

「这是我参与11月更文挑战的第29天,活动详情查看:2021最后一次更文挑战

确保Web应用程序的安全是一个固有的复杂命题。Spring Security为Java开发者提供了一个强大的框架来满足这一需求,但这一强大的工具伴随着一个陡峭的学习曲线。

本文简要介绍了用Spring Security保护REST API背后的基本组件。我们将建立一个简单的应用,使用JSON Web Token(JWT)来存储用户的信息。

JWT由于其简单性和紧凑性,正在迅速成为持有授权信息的标准方法。

认证过滤器 TokenAuthenticationFilter.java

TokenAuthenticationFilter负责检查进入受保护URL的请求。这项工作在清单4中完成。

清单4.过滤器的逻辑

@Override
  public Authentication attemptAuthentication(final HttpServletRequest request,
                                              final HttpServletResponse response) {
    final String param = ofNullable(request.getHeader(AUTHORIZATION)).orElse(request.getParameter("t"));

    final String token = ofNullable(param).map(value -> removeStart(value, "Bearer"))
      .map(String::trim).orElseThrow(() -> new BadCredentialsException("No Token Found!"));

    final Authentication auth = new UsernamePasswordAuthenticationToken(token, token);
    return getAuthenticationManager().authenticate(auth);
  }

  @Override
  protected void successfulAuthentication(final HttpServletRequest request,
        final HttpServletResponse response, final FilterChain chain,
        final Authentication authResult) throws IOException, ServletException {
      super.successfulAuthentication(request, response, chain, authResult);
      chain.doFilter(request, response);
  }
复制代码

基本上,过滤器从授权头中提取token(由前端JS发送的那个)。如果它不在那里,就会产生一个异常。如果它在那里,它就会被移交给认证管理器,在那里它最终会被你刚才在SecurityConfig中看到的TokenAuthenticationProvider处理。

检查token TokenAuthenticationProvider.java

TokenAuthenticationProvider负责根据authtoken恢复用户。它只有一个方法,将其工作委托给UserAuthenticationService,如清单5中所示。

清单5.TokenAuthenticationProvider.retrieveUser()

@Autowired
UserAuthenticationService auth;
//...
@Override
  protected UserDetails retrieveUser(final String username, final UsernamePasswordAuthenticationToken authentication) {
    final Object token = authentication.getCredentials();
    return Optional.ofNullable(token).map(String::valueOf).flatMap(auth::findByToken)
      .orElseThrow(() -> new UsernameNotFoundException("Couldn't find user: " + token));
  }
复制代码

如果用户为空,就会产生一个异常。

UserAuthenticationService.java 和TokenAuthenticationService.java

TokenAuthenticationService是将被自动连接到TokenAuthenticationProvider的实现。它提供了用于检索用户的findByToken方法

TokenAuthenticationService也是登录流程与认证流程结合的地方。它提供了UserController使用的login()方法。在清单6中可以看到这两个方法。

清单6.TokenAuthenticationService方法

@Autowired
  TokenService tokenService;
  @Autowired
  UserService users;

  @Override
  public Optional<String> login(final String username, final String password) {
    return users
      .findByUsername(username)
      .filter(user -> Objects.equals(password, user.getPassword()))
      .map(user -> tokenService.newToken(ImmutableMap.of("username", username)));
  }

  @Override
  public Optional<User> findByToken(final String token) {
    System.out.println("$$$$$$$$$$$$$$$$$$$$ token: " + token);
    return Optional
      .of(tokenService.verify(token))
      .map(map -> map.get("username"))
      .flatMap(users::findByUsername);
  }
复制代码

两个方法--findByTokenlogin--都依赖于TokenServiceUserService findByToken需要一个token,然后使用tokenService来验证其有效性。如果token是好的,findByToken使用UserService来获取实际的用户对象。

login的做法正好相反。它需要一个用户名,用userService抓取用户,验证密码是否匹配,然后用tokenService来创建token。

TokenService.java和JWTTokenService.java

JWTTokenService是处理实际JWTtoken的地方。它依靠JJWT库来完成工作,如清单7所示。

清单7.JWTTokenService

JWTTokenService() {
    super();
    this.issuer = requireNonNull("infoworld");
    this.secretKey = BASE64.encode("www.infoworld.com");
  }
  public String newToken(final Map<String, String> attributes) {
    final DateTime now = DateTime.now();
    final Claims claims = Jwts.claims().setIssuer(issuer).setIssuedAt(now.toDate());

    claims.putAll(attributes);

    return Jwts.builder().setClaims(claims).signWith(HS256, secretKey).compressWith(COMPRESSION_CODEC)
      .compact();
  }

  @Override
  public Map<String, String> verify(final String token) {
    final JwtParser parser = Jwts.parser().requireIssuer(issuer).setClock(this).setSigningKey(secretKey);
    return parseClaims(() -> parser.parseClaimsJws(token).getBody());
  }

  private static Map<String, String> parseClaims(final Supplier<Claims> toClaims) {
    try {
      final Claims claims = toClaims.get();
      final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
      for (final Map.Entry<String, Object> e: claims.entrySet()) {
        builder.put(e.getKey(), String.valueOf(e.getValue()));
      }
      return builder.build();
    } catch (final IllegalArgumentException | JwtException e) {
      return ImmutableMap.of();
    }
  }
复制代码

JJWT库使创建、解析和验证JWTtoken变得相当容易。newToken()方法使用Jwts.claim()来设置几个标准的claim(发行者和发行地点)以及作为参数传入的任何其他claim。在登录的情况下,这将包含用户的名字。这意味着用户名可以在以后的认证过程中被反序列化。在这一点上,应用程序也可以添加其他索赔,如角色或明确的权限类型。

感谢观看,如果您有兴趣,可以关注一下我,方便查看后续文章,一起学习,共同进步,不胜感激!