使用带JWT标记的角度弹簧和反作用弹簧


这个项目AngularAndSpring提供了一个使用JWT令牌保护的角度弹簧和反作用弹簧的示例。这AngularAndSpring项目可以使用嵌入式蒙古数据库进行简单的本地测试。对于角度和弹簧中的反应式编程的主题,这里有一个article更多细节。

角形和弹簧做什么?

这是一个应用程序,显示了四种交易类型的密码货币报价。货币有详细的页面,包括每日/每周/每月图表和图表上的数据报告。登录后可以使用订单簿页面,可以看到订单簿。订单数据是从交易所请求的,因此,页面需要得到保护。

JWT令牌

使用JWT令牌是因为它们可以保护对REST接口的访问,而无需浏览器和服务器之间的会话。令牌是base64编码的,由服务器签名。浏览器使用用户名和密码登录,并获得一个令牌作为响应。然后,令牌被存储在浏览器中,然后,在所有安全请求中,以超文本传输协议头的形式发送到服务器。服务器可以检查签名和过期,然后提供对REST应用编程接口的访问,使应用编程接口无状态和安全。

配置

弹簧靴的安全配置在WebSecurityConfig

@Configuration
@Order(SecurityProperties.DEFAULT_FILTER_ORDER)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private MyAuthenticationProvider authProvider;

  @Autowired
  private JwtTokenProvider jwtTokenProvider;

  @Override
  protected void configure(HttpSecurity http) throws Exception {
     http.httpBasic();
     http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
     http.authorizeRequests().anyRequest().permitAll().anyRequest().anonymous();
     http.antMatcher("/**/orderbook").authorizeRequests().anyRequest().authenticated();
     http.csrf().disable();
     http.apply(new JwtTokenFilterConfigurer(jwtTokenProvider));
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(authProvider);
  }

}


第14行将JWT令牌的会话创建策略设置为无状态。

第15行将默认权限设置为匿名,以提供无需令牌的访问。

第16行为orderbook REST应用编程接口设置了使用令牌进行身份验证的权限。

第18行设置了JwtTokenFilterConfigurerJwtTokenProvider

JWT代币供应商

JwtTokenProviders类创建并处理JWT令牌:

@Component
public class JwtTokenProvider {

  @Value("${security.jwt.token.secret-key}")
  private String secretKey;

  @Value("${security.jwt.token.expire-length}")
  private long validityInMilliseconds; // 24h

  @Autowired
  private ReactiveMongoOperations operations;

  public String createToken(String username, List<Role> roles) {
    Claims claims = Jwts.claims().setSubject(username);
    claims.put("auth", roles.stream().map(s -> new SimpleGrantedAuthority(s.getAuthority()))
    .filter(Objects::nonNull).collect(Collectors.toList()));

    Date now = new Date();
    Date validity = new Date(now.getTime() + validityInMilliseconds);
    String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());

    return Jwts.builder().setClaims(claims).setIssuedAt(now).setExpiration(validity)
    .signWith(SignatureAlgorithm.HS256, encodedSecretKey).compact();
  }

  public Optional<Jws<Claims>> getClaims(Optional<String> token) {
    if (!token.isPresent()) {
      return Optional.empty();
    }
    String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    return Optional.of(Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token.get()));
  }

  public Authentication getAuthentication(String token) {
    Query query = new Query();
    query.addCriteria(Criteria.where("userId").is(getUsername(token)));
    MyUser user = operations.findOne(query, MyUser.class).block();

    return new UsernamePasswordAuthenticationToken(user, "", user.getAuthorities());
  }

  public String getUsername(String token) {
    String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    return Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token).getBody().getSubject();
  }

  public String resolveToken(HttpServletRequest req) {
    String bearerToken = req.getHeader("Authorization");
    if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
      return bearerToken.substring(7, bearerToken.length());
    }
    return null;
  }

  public boolean validateToken(String token) {
    String encodedSecretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    try {
      Jwts.parser().setSigningKey(encodedSecretKey).parseClaimsJws(token);
      return true;
    } catch (JwtException | IllegalArgumentException e) {
      throw new RuntimeException("Expired or invalid JWT token");
    }
  } 

}


第13-24行为提供的用户名和角色创建了一个JWT令牌。令牌的声明是Spring Boot的角色。到期时间由validityInMilliseconds 设置在application.properties。签署JWT令牌的密钥由secretKey 也设置在application.properties并且需要base64编码。

第26-32行对secretKey阅读令牌的声明。

第34-40行创建一个UsernamePasswordToken 基于蒙古数据库中用户的值,用于过滤器链中的身份验证。

第42-45行读取JWT令牌的用户名。

第47-53行从HTTP头中读取JWT令牌,并返回令牌字符串。

第55-63行用编码的secretKey。这确保了令牌没有被篡改。

登录过程

登录是通过MyUserController提供登录应用编程接口:

@PostMapping("/login")
public Mono<MyUser> postUserLogin(@RequestBody MyUser myUser,HttpServletRequest request)
  throws NoSuchAlgorithmException, InvalidKeySpecException {
  Query query = new Query();
  query.addCriteria(Criteria.where("userId").is(myUser.getUserId()));
  return this.operations.findOne(query, MyUser.class).switchIfEmpty(Mono.just(new MyUser()))
  .map(user1 -> loginHelp(user1, myUser.getPassword()));
}

private MyUser loginHelp(MyUser user, String passwd) {
  if (user.getUserId() != null) {
    String encryptedPassword;
    try {
      encryptedPassword = this.passwordEncryption.getEncryptedPassword(passwd, user.getSalt());
    } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
      return new MyUser();
    }
    if (user.getPassword().equals(encryptedPassword)) {
      String jwtToken = this.jwtTokenProvider.createToken(user.getUserId(), Arrays.asList(Role.USERS));
      user.setToken(jwtToken);
      user.setPassword("XXX");
      return user;
    }
  }
  return new MyUser();
}


在第1-8行,提供了登录的应用编程接口调用。用户必须发布一个MyUser 有用户名,密码,盐。然后,为userId 从蒙古数据库中检索并映射到新的MyUser 对象。

在第10-26行,用户提供的密码被加密,并与数据库中的密码进行核对。如果密码正确,Jwt令牌将被创建并添加到用户的MyUser对象中,密码将被删除。然后,用户获得带有Jwt令牌的MyUser对象。

角度登录服务

登录服务myuser.service注意登录的角度:

@Injectable()
export class MyuserService {
  private _reqOptionsArgs= { headers: new HttpHeaders().set( 'Content-Type', 'application/json' ) };
  private _utils = new Utils();
  private myUserUrl = "/myuser";

  constructor(private http: HttpClient, private pl: PlatformLocation ) { 
  }

  postLogin(user: MyUser): Observable<MyUser> {
      return this.http.post<MyUser>(this.myUserUrl+'/login', user, this._reqOptionsArgs).pipe(map(res => {
          let retval = <MyUser>res;
          localStorage.setItem("salt", retval.salt);  
          localStorage.setItem("token", retval.token);
          return retval;
      }),catchError(this._utils.handleError<MyUser>('postLogin')));      
  }


在第10-16行,带有用户名和密码的用户对象被发送到服务器,并处理响应。盐和回应的标记被放入LocalStorage用于订购单请求。

角度订购单请求

对其中一个订单簿的请求在中完成bitfinex.service

 getOrderbook(currencypair: string): Observable<OrderbookBf> {
      let reqOptions = {headers: this._utils.createTokenHeader()};
      return this.http.get<OrderbookBf>(this._bitfinex+'/'+currencypair+'/orderbook/', reqOptions).pipe(catchError(this._utils.handleError<OrderbookBf>('getOrderbook')));
  }


这里,一个普通的get请求被发送到服务器,在reqOptions

中添加了以下令牌utils

export class Utils {

    get token():string {
        return !localStorage.getItem("token") ? null : localStorage.getItem("token");
    }

    public createTokenHeader(): HttpHeaders {
        let reqOptions = new HttpHeaders().set( 'Content-Type', 'application/json' )
        if(this.token) {
            reqOptions = new HttpHeaders().set( 'Content-Type', 'application/json' ).set('Authorization', 'Bearer ' + this.token);
        }
        return reqOptions;
    }


在第3-5行中,创建了一个getter,它从LocalStorage 或null。

在第7-13行,建立了HTTP头。如果令牌存在于LocalStorage,添加带有载体和令牌的授权。

摘要

这个项目AngularAndSpring显示了角形、弹簧靴、弹簧安全和JWT令牌可以一起工作。后端是无状态的,这使得它可以横向扩展。应用程序的匿名部分使用了Spring Boot 2的反应特性。前端使用带材质的角度,有图表、动画,支持多种语言。它可以从内存中的蒙古数据库开始,现在将独立测试。