内容简介:使用JWT实现安全认证——基于Angular2和Spring
= 27
安全认证一直都是应用非常重要的一环,并且随着移动app和SPA的不断发展,基于token的认证正不断成为主流。JSON Web Tokens是一种简单好用的认证方法,一个JWT类型的token一般由三部分组成,分别是Header、Payload和Signature,当然有时候会加一个token前缀。
以下图为例,分析一下JWT的结构:
首先,前面的“Bearer ”是token前缀,后面的内容中由两个”.”分割成为了三份。它们便分别对应Header,Payload和Signature。
-
Header : 记录token算法和类型的字段
-
Payload:记录所有数据的JSON对象,真正有效的数据都包含在这里
-
Signature:Signature是签名动作发生的地方,为了得到签名,我们使用Base64URL编码头部,接着使用Base64URL编码payoad,然后把这段字符串和密钥一起使用哈希算法加密,这个计算的结果就是signature。如果token中的信息被篡改了,服务端可以根据签名校验发现这个行为。
根据刚才的分析,我们可以知道,token的有效信息其实是没有加密的。JWT的机制是保证了token的不可篡改性,但是不适合用来保存敏感信息。以上图中的token为例,它的Payload内容如下图:
关于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
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 安全及数字认证等使用
- ActiveMQ学习-安全认证-连接AMQ用户密码配置 (2)
- 『高级篇』docker之安全认证kubernetes命令熟悉(40)
- 一文看懂认证安全问题总结篇
- UWeb v1.1.0 发布,优化 Shiro 安全认证模块
- Spring Cloud Security&Eureka安全认证(Greenwich版本)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。