使用JWT实现安全认证——基于Angular2和Spring

栏目: Java · 发布时间: 7年前

内容简介:使用JWT实现安全认证——基于Angular2和Spring

= 27

本文出自:【InTheWorld的博客】

使用JWT实现安全认证——基于Angular2和Spring

安全认证一直都是应用非常重要的一环,并且随着移动app和SPA的不断发展,基于token的认证正不断成为主流。JSON Web Tokens是一种简单好用的认证方法,一个JWT类型的token一般由三部分组成,分别是Header、Payload和Signature,当然有时候会加一个token前缀。

以下图为例,分析一下JWT的结构:

使用JWT实现安全认证——基于Angular2和Spring

首先,前面的“Bearer ”是token前缀,后面的内容中由两个”.”分割成为了三份。它们便分别对应Header,Payload和Signature。

  • Header 记录token算法和类型的字段

  • Payload:记录所有数据的JSON对象,真正有效的数据都包含在这里

  • Signature:Signature是签名动作发生的地方,为了得到签名,我们使用Base64URL编码头部,接着使用Base64URL编码payoad,然后把这段字符串和密钥一起使用哈希算法加密,这个计算的结果就是signature。如果token中的信息被篡改了,服务端可以根据签名校验发现这个行为。

根据刚才的分析,我们可以知道,token的有效信息其实是没有加密的。JWT的机制是保证了token的不可篡改性,但是不适合用来保存敏感信息。以上图中的token为例,它的Payload内容如下图:

使用JWT实现安全认证——基于Angular2和Spring

关于JWT的基本介绍,就先到这里,接下来的内容是如何使用Angular2和Spring Boot实现一个简单SPA认证应用。

使用Angualr2构建SPA

Angular作为一个非常优秀的前端框架,应该很少有人没有听说过。从个人来说,我是以前玩MEAN Stack的时候真正意义上的接触Angular的(MEAN是mongoDB, Express, AngularJS和Node.js的缩写)。对Angular的第一感觉就是“这个一个野心勃勃的框架”,后来由于对JS写复杂逻辑时的问题难以忍受(其实主要还是不满它在服务端的表现),我在一段时间内没有继续对AngularJS的学习。后来Google中国开发者大会上对AngularJS大讲特讲,随后AngularJS版本大变,并且全面转移到TypeScript。我听到这个消息,其实心里就有点痒痒了,毕竟Web app才是主流。而且,我对强类型语言的依赖性非常强,TypeScript也是我的菜。

废话讲了一大段,还是说点正经的吧!首先,我们还是需要一个登陆页面,主要代码如下所示:

import { Component } from '@angular/core'
import {Http, Response} from "@angular/http";
import {Router} from "@angular/router";
import { contentHeaders } from '../common/headers';

const styles = require('./login.css');
const template = require('./login.html')

@Component({
  selector: 'login',
  template: template,
  styles: [ styles ]
})

export class Login {
  constructor(public router: Router, public http: Http) {

  }

  login(event, username: string, password: string): void {
    event.preventDefault();
    let body = JSON.stringify( { username, password });
    this.http.post('http://localhost:8080/login', body, { headers: contentHeaders})
      .subscribe(
        (response: Response) => {
          localStorage.setItem('id_token', response.headers.get("Authorization"));
          console.log(response.headers.get("Authorization"));
          this.router.navigate(['home']);
        },
        error => {
          alert(error.text());
          console.log(error.text());
        }
      );
  }

  signup(event): void {
    event.preventDefault();
    this.router.navigate(['signup']);
  }

}

登陆页面的逻辑其实很简单,就是把用户名和密码以JSON格式传递给服务器,以换取JWT token。如果成功的话,把token记录在localStorage中,然后导航到home页。home页有基本的token信息,以及实现一些普通和被保护的endpoint的访问。home页面的代码如下:

import {Component} from "@angular/core";
import {Headers, Http} from "@angular/http";
import {Router} from "@angular/router";
import { JwtHelper, AuthHttp} from "angular2-jwt"

const styles = require('./home.css');
const template = require('./home.html');

@Component({
  selector: 'home',
  template: template,
  styles: [ styles ]
})

export class Home {
  jwt: string;
  decodedJwt: string;
  response: string;
  api: string;

  constructor(public router: Router, public http: Http, public authHttp: AuthHttp) {
    this.jwt = localStorage.getItem('id_token');
    this.decodedJwt = this.jwt && (new JwtHelper()).decodeToken(this.jwt);
  }

  logout() {
    localStorage.removeItem('id_token');
    this.router.navigate(['login']);
  }

  callAnonymousApi() {
    this._callApi('Anonymous', 'http://localhost:8080/');
  }

  callSecuredApi() {
    this._callApi('Secured', 'http://localhost:8080/users');
  }

  _callApi(type, url) {
    this.response = null;
    if (type === 'Anonymous') {
      // For non-protected routes, just use Http
      this.http.get(url)
        .subscribe(
          response => this.response = response.text(),
          error => this.response = error.text()
        );
    }
    if (type === 'Secured') {
      // For protected routes, use AuthHttp
      const authHeader = new Headers();
      authHeader.append('Authorization', this.jwt);
      console.log(this.jwt);
      this.http.get(url, { headers: authHeader })
        .subscribe(
          response => this.response = response.text(),
          error => this.response = error.text()
        );
    }
  }
}

其他的一些代码详见github,我会附在文章后面。

使用Spring Boot构建认证应用

我主要使用Spring security来完成token授权以及token认证。token授权(其实就是生成)主要由JWTLoginFilter完成,它的完整代码如下所示:

public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JWTLoginFilter(String url, AuthenticationManager authManager) {
        super(new AntPathRequestMatcher(url, "POST"));
        setAuthenticationManager(authManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        Enumeration<String> paramNames = request.getParameterNames();//获取所有的参数名
        while (paramNames.hasMoreElements()) {
            String name = paramNames.nextElement();//得到参数名
            String value = request.getParameter(name);//通过参数名获取对应的值
            System.out.println(MessageFormat.format("{0}={1}", name,value));
        }
        String contentType = request.getContentType();
        if(contentType.contains("application/x-www-form-urlencoded")){//web表单post
            System.out.println("web post");
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            username,
                            password,
                            Collections.emptyList()
                    )
            );
        }else{
            //app或者ajax post,使用json解析
            AccountCredentials creds = new ObjectMapper()
                    .readValue(request.getInputStream(), AccountCredentials.class);
            System.out.println("json post");
            response.addHeader("Access-Control-Allow-Origin", "*");  //此优先级高于@CrossOrigin配置

            // Access-Control-Allow-Methods: 授权请求的方法(GET, POST, PUT, DELETE,OPTIONS等)
            response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");

            response.addHeader("Access-Control-Allow-Headers", "Content-Type");
            response.addHeader("Access-Control-Expose-Headers", "Authorization");

            response.addHeader("Access-Control-Max-Age", "1800");//30 min
            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            creds.getUsername(),
                            creds.getPassword(),
                            Collections.emptyList()
                    )
            );
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
        TokenAuthenticationService.addAuthentication(res, auth.getName());
    }
}

值得一提的是,这个Filter兼容了SPA登陆以及web post登陆两种方式。一般来说SPA会使用JSON形式的body,而浏览器原生表单的post只能使用x-www-form-urlencoded这种包体。此外,另外一个filter利用TokenAuthenticationService实现了token的认证,TokenAuthenticationService的主要代码如下。

public class TokenAuthenticationService {
    static final long EXPIRATIONTIME = 864_000_000;
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    static void addAuthentication(HttpServletResponse res, String username) {
        String JWT = Jwts.builder()
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT);
    }

    static Authentication getAuthentication(HttpServletRequest request) {
        String token = request.getHeader(HEADER_STRING);
        if (token != null) {
            String user = Jwts.parser()
                    .setSigningKey(SECRET)
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                    .getBody()
                    .getSubject();

            return user != null ?
                    new UsernamePasswordAuthenticationToken(user, null, emptyList()) : null;
        }
        return null;
    }

}

事实上,这个TokenAuthenticationService也包含了token生成的逻辑。而且在这个demo里面,仅仅使用了用户名的匹配进行token的校验。这是一种非常粗略的方式,在生产环境是非常不可取的。

需要注意的问题

由于本文有不少的背景知识,很难非常系统的把所有问题都讲清楚。对前后端的介绍也只能点到为止了,接下来主要总结一下个人在实现这个demo遇到的一些问题。

  • CORS的问题

CORS是Cross-Origin Resource Sharing的缩写,这本质上来自于浏览器的保护机制。当web页面的host与Ajax访问的host不同时,浏览器会使用Http的OPTIONS方法,去探测被访问的host是否允许跨域访问。我的AngularJS跑在localhost:4200,而Spring boot跑在localhost:8080。所以需要在Spring Boot侧允许这种跨域访问。具体来说就是添加Access-Control-Allow-Origin的头,如下这样的方式:

response.addHeader("Access-Control-Allow-Origin", "*");

此外,还有一个问题,JWT的token以自定义header的方式存在,还需要添加Access-Control-Expose-Headers的头,其中Authorization为token的名称。如果不添加这个header,Ajax拿不到这个token的值,更完成不了认证。

response.addHeader("Access-Control-Expose-Headers", "Authorization");
  • Angular2-jwt的问题

现在Angular的版本已经到了4.0+,而Angular2-jwt的更新速度一直没有跟上,所以有一些bug。然而也不能因为Angular2-jwt的问题而不进步吧!为此,我的解决办法是,仅仅使用Angular2-jwt中JwtHelper的功能,其他的自己写。实话说来,也写不了几行代码,反而会让我们更加清楚jwt的使用原理。

  • 对OPTIONS请求的过滤

和第一问题一样,浏览器会进行一个preflight的OPTIONS请求。而Spring security对非授权访问的默认行为是页面跳转(30x),这个OPTIONS是浏览器的(不会自己加JSON Web Token),如果OPTIONS跳转了,会导致接下来的正式访问也出问题。所以需要把Spring security配置为不拦截OPTIONS请求,问题随之解决。

好吧,这篇blog就先到这里了。

Angular2 SPA: https://github.com/intheworld/authorization

Spring Boot Application: https://github.com/intheworld/xforce


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Designing Web Navigation

Designing Web Navigation

James Kalbach / O'Reilly Media / 2007-8-15 / USD 49.99

Thoroughly rewritten for today's web environment, this bestselling book offers a fresh look at a fundamental topic of web site development: navigation design. Amid all the changes to the Web in the pa......一起来看看 《Designing Web Navigation》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具