Java JWT: JSON Web Token

 

Java JWT: JSON Web Token for Java and Android

JJWT aims to be the easiest to use and understand library for creating and verifying JSON Web Tokens (JWTs) on the JVM.css

JJWT is a Java implementation based on the JWTJWSJWEJWK and JWA RFC specifications.html

The library was created by Stormpath's CTO, Les Hazlewood and is now maintained by a community of contributors.html5

Stormpath is a complete authentication and user management API for developers.java

We've also added some convenience extensions that are not part of the specification, such as JWT compression and claim enforcement.react

What's a JSON Web Token?

Don't know what a JSON Web Token is? Read on. Otherwise, jump on down to the Installation section.nginx

JWT is a means of transmitting information between two parties in a compact, verifiable form.git

The bits of information encoded in the body of a JWT are called claims. The expanded form of the JWT is in a JSON format, so each claim is a key in the JSON object.github

JWTs can be cryptographically signed (making it a JWS) or encrypted (making it a JWE).web

This adds a powerful layer of verifiability to the user of JWTs. The receiver has a high degree of confidence that the JWT has not been tampered with by verifying the signature, for instance.算法

The compacted representation of a signed JWT is a string that has three parts, each separated by a .:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.ipevRNuRP6HflG8cFKnmUPtypruRC4fb1DWtoLL62SY

Each section is base 64 encoded. The first section is the header, which at a minimum needs to specify the algorithm used to sign the JWT. The second section is the body. This section has all the claims of this JWT encoded in it. The final section is the signature. It's computed by passing a combination of the header and body through the algorithm specified in the header.

If you pass the first two sections through a base 64 decoder, you'll get the following (formatting added for clarity):

header

{
  "alg": "HS256"
}

body

{
  "sub": "Joe"
}

In this case, the information we have is that the HMAC using SHA-256 algorithm was used to sign the JWT. And, the body has a single claim, sub with value Joe.

There are a number of standard claims, called Registered Claims, in the specification and sub (for subject) is one of them.

To compute the signature, you must know the secret that was used to sign it. In this case, it was the word secret. You can see the signature creation is action here (Note: Trailing = are lopped off the signature for the JWT).

Now you know (just about) all you need to know about JWTs.

Installation

Use your favorite Maven-compatible build tool to pull the dependency (and its transitive dependencies) from Maven Central:

Maven:

<dependency>
    <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>

Gradle:

dependencies {
    compile 'io.jsonwebtoken:jjwt:0.7.0' }

Note: JJWT depends on Jackson 2.x. If you're already using an older version of Jackson in your app, read this

Quickstart

Most complexity is hidden behind a convenient and readable builder-based fluent interface, great for relying on IDE auto-completion to write code quickly. Here's an example:

import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.crypto.MacProvider; import java.security.Key; // We need a signing key, so we'll create one just for this example. Usually // the key would be read from your application configuration instead. Key key = MacProvider.generateKey(); String compactJws = Jwts.builder() .setSubject("Joe") .signWith(SignatureAlgorithm.HS512, key) .compact();

How easy was that!?

In this case, we are building a JWT that will have the registered claim sub (subject) set to Joe. We are signing the JWT using the HMAC using SHA-512 algorithm. finally, we are compacting it into its String form.

The resultant String looks like this:

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJKb2UifQ.yiV1GWDrQyCeoOswYTf_xvlgsnaVVYJM0mU6rkmRBf2T1MBl3Xh2kZii0Q9BdX5-G0j25Qv2WF4lA6jPl5GKuA

Now let's verify the JWT (you should always discard JWTs that don't match an expected signature):

assert Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody().getSubject().equals("Joe");

There are two things going on here. The key from before is being used to validate the signature of the JWT. If it fails to verify the JWT, a SignatureException is thrown. Assuming the JWT is validated, we parse out the claims and assert that that subject is set to Joe.

You have to love code one-liners that pack a punch!

But what if signature validation failed? You can catch SignatureException and react accordingly:

try {

    Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws); //OK, we can trust this JWT } catch (SignatureException e) { //don't trust the JWT! }

Supported Features

Specification Compliant:

  • Creating and parsing plaintext compact JWTs

  • Creating, parsing and verifying digitally signed compact JWTs (aka JWSs) with all standard JWS algorithms:

    • HS256: HMAC using SHA-256
    • HS384: HMAC using SHA-384
    • HS512: HMAC using SHA-512
    • RS256: RSASSA-PKCS-v1_5 using SHA-256
    • RS384: RSASSA-PKCS-v1_5 using SHA-384
    • RS512: RSASSA-PKCS-v1_5 using SHA-512
    • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
    • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
    • PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
    • ES256: ECDSA using P-256 and SHA-256
    • ES384: ECDSA using P-384 and SHA-384
    • ES512: ECDSA using P-521 and SHA-512

Enhancements Beyond the Specification:

  • Body compression. If the JWT body is large, you can use a CompressionCodec to compress it. Best of all, the JJWT library will automtically decompress and parse the JWT without additional coding.

    String compactJws = Jwts.builder() .setSubject("Joe") .compressWith(CompressionCodecs.DEFLATE) .signWith(SignatureAlgorithm.HS512, key) .compact();

    If you examine the header section of the compactJws, it decodes to this:

    {
      "alg": "HS512",
      "zip": "DEF"
    }

    JJWT automatically detects that compression was used by examining the header and will automatically decompress when parsing. No extra coding is needed on your part for decompression.

  • Require Claims. When parsing, you can specify that certain claims must be present and set to a certain value.

    try {
        Jws<Claims> claims = Jwts.parser() .requireSubject("Joe") .require("hasMotorcycle", true) .setSigningKey(key) .parseClaimsJws(compactJws); } catch (MissingClaimException e) { // we get here if the required claim is not present } catch (IncorrectClaimException e) { // we get here if the required claim has the wrong value }

Currently Unsupported Features

  • Non-compact serialization and parsing.
  • JWE (Encryption for JWT)

These feature sets will be implemented in a future release. Community contributions are welcome!

Learn More

Already using an older Jackson dependency?

JJWT depends on Jackson 2.8.x (or later). If you are already using a Jackson version in your own application less than 2.x, for example 1.9.x, you will likely see runtime errors. To avoid this, you should change your project build configuration to explicitly point to a 2.x version of Jackson. For example:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.8.2</version> </dependency>

Author

Maintained by Stormpath

http://www.cnblogs.com/softidea/p/6204673.html

 

https://github.com/jwtk/jjwt

4.1. Registered Claim Names



   The following Claim Names are registered in the IANA "JSON Web Token
   Claims" registry established by Section 10.1.  None of the claims
   defined below are intended to be mandatory to use or implement in all
   cases, but rather they provide a starting point for a set of useful,
   interoperable claims.  Applications using JWTs should define which
   specific claims they use and when they are required or optional.  All
   the names are short because a core goal of JWTs is for the
   representation to be compact.

4.1.1. "iss" (Issuer) Claim

 The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this claim is generally application specific. The "iss" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL. 

4.1.2. "sub" (Subject) Claim

 The "sub" (subject) claim identifies the principal that is the subject of the JWT. The claims in a JWT are normally statements about the subject. The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. The processing of this claim is generally application specific. The "sub" value is a case-sensitive string containing a StringOrURI value. Use of this claim is OPTIONAL. 

4.1.3. "aud" (Audience) Claim

 The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal intended to process the JWT MUST identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the "aud" claim when this claim is present, then the JWT MUST be rejected. In the general case, the "aud" value is an array of case- sensitive strings, each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience values is generally application specific. Use of this claim is OPTIONAL. 

4.1.4. "exp" (Expiration Time) Claim

 The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. The processing of the "exp" claim requires that the current date/time MUST be before the expiration date/time listed in the "exp" claim.

 https://tools.ietf.org/html/rfc7519#section-4.1

 

 

 

 

原文  https://juejin.im/post/58c29e0b1b69e6006bce02f4

 

一般状况下,把API直接暴露出去是风险很大的,不说别的,直接被机器攻击就喝一壶的。那么通常来讲,对API要划分出必定的权限级别,而后作一个用户的鉴权,依据鉴权结果给予用户开放对应的API。目前,比较主流的方案有几种:

  1. 用户名和密码鉴权,使用Session保存用户鉴权结果。
  2. 使用OAuth进行鉴权(其实OAuth也是一种基于Token的鉴权,只是没有规定Token的生成方式)
  3. 自行采用Token进行鉴权

第一种就不介绍了,因为依赖Session来维护状态,也不太适合移动时代,新的项目就不要采用了。第二种OAuth的方案和JWT都是基于Token的,但OAuth其实对于不作开放平台的公司有些过于复杂。咱们主要介绍第三种:JWT。

什么是JWT?

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种能够安全传输的 小巧 和 自包含 的JSON对象。因为数据是使用数字签名的,因此是可信任的和安全的。JWT可使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

JWT的工做流程

下面是一个JWT的工做流程图。模拟一下实际的流程是这样的(假设受保护的API在 /protected 中)

  1. 用户导航到登陆页,输入用户名、密码,进行登陆
  2. 服务器验证登陆鉴权,若是改用户合法,根据用户的信息和服务器的规则生成JWT Token
  3. 服务器将该token以json形式返回(不必定要json形式,这里说的是一种常见的作法)
  4. 用户获得token,存在localStorage、cookie或其它数据存储形式中。
  5. 之后用户请求 /protected 中的API时,在请求的header中加入 Authorization: Bearer xxxx(token) 。此处注意token以前有一个7字符长度的 Bearer
  6. 服务器端对此token进行检验,若是合法就解析其中内容,根据其拥有的权限和本身的业务逻辑给出对应的响应结果。
  7. 用户取得结果

JWT工做流程图

为了更好的理解这个token是什么,咱们先来看一个token生成后的样子,下面那坨乱糟糟的就是了。

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ.RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg

但仔细看到的话仍是能够看到这个token分红了三部分,每部分用 . 分隔,每段都是用 Base64编码的。若是咱们用一个Base64的解码器的话 ( https://www.base64decode.org/ ),能够看到第一部分 eyJhbGciOiJIUzUxMiJ9 被解析成了:

{
    "alg":"HS512" }

这是告诉咱们HMAC采用HS512算法对JWT进行的签名。

第二部分 eyJzdWIiOiJ3YW5nIiwiY3JlYXRlZCI6MTQ4OTA3OTk4MTM5MywiZXhwIjoxNDg5Njg0NzgxfQ 被解码以后是

{
    "sub":"wang", "created":1489079981393, "exp":1489684781 }

这段告诉咱们这个Token中含有的数据声明(Claim),这个例子里面有三个声明: sub , created 和 exp 。在咱们这个例子中,分别表明着用户名、建立时间和过时时间,固然你能够把任意数据声明在这里。

看到这里,你可能会想这是个什么鬼token,全部信息都透明啊,安全怎么保障?别急,咱们看看token的第三段 RC-BYCe_UZ2URtWddUpWXIp4NMsoeq2O6UF-8tVplqXY1-CI9u1-a-9DAAJGfNWkHE81mpnR3gXzfrBAB3WUAg 。一样使用Base64解码以后,咦,这是什么东东

D X    DmYTeȧLUZcPZ0$gZAY_7wY@

最后一段实际上是签名,这个签名必须知道秘钥才能计算。这个也是JWT的安全保障。这里提一点注意事项,因为数据声明(Claim)是公开的,千万不要把密码等敏感字段放进去,不然就等因而公开给别人了。

也就是说JWT是由三段组成的,按官方的叫法分别是header(头)、payload(负载)和signature(签名):

header.payload.signature

头中的数据一般包含两部分:一个是咱们刚刚看到的 alg ,这个词是 algorithm 的缩写,就是指明算法。另外一个能够添加的字段是token的类型(按RFC 7519实现的token机制不仅JWT一种),但若是咱们采用的是JWT的话,指定这个就多余了。

{
  "alg": "HS512", "typ": "JWT" }

payload中能够放置三类数据:系统保留的、公共的和私有的:

  • 系统保留的声明(Reserved claims):这类声明不是必须的,可是是建议使用的,包括:iss (签发者), exp (过时时间), 
    sub (主题), aud (目标受众)等。这里咱们发现都用的缩写的三个字符,这是因为JWT的目标就是尽量小巧。
  • 公共声明:这类声明须要在 IANA JSON Web Token Registry 中定义或者提供一个URI,由于要避免重名等冲突。
  • 私有声明:这个就是你根据业务须要本身定义的数据了。

签名的过程是这样的:采用header中声明的算法,接受三个参数:base64编码的header、base64编码的payload和秘钥(secret)进行运算。签名这一部分若是你愿意的话,能够采用RSASHA256的方式进行公钥、私钥对的方式进行,若是安全性要求的高的话。

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

JWT的生成和解析

为了简化咱们的工做,这里引入一个比较成熟的JWT类库,叫 jjwt ( https://github.com/jwtk/jjwt )。这个类库能够用于Java和Android的JWT token的生成和验证。

JWT的生成可使用下面这样的代码完成:

String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) //采用什么算法是能够本身选择的,不必定非要采用HS512 .compact(); }

数据声明(Claim)其实就是一个Map,好比咱们想放入用户名,能够简单的建立一个Map而后put进去就能够了。

Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, username());

解析也很简单,利用 jjwt 提供的parser传入秘钥,而后就能够解析token了。

Claims getClaimsFromToken(String token) {
    Claims claims; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { claims = null; } return claims; }

JWT自己没啥难度,但安全总体是一个比较复杂的事情,JWT只不过提供了一种基于token的请求验证机制。但咱们的用户权限,对于API的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。因此咱们这里要利用Spring的一个子项目Spring Security来简化咱们的工做。

Spring Security

Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一块儿来完成API保护。因此关于Spring Secruity的基础内容或展开内容,请自行去官网学习( http://projects.spring.io/spring-security/ )。

简单的背景知识

若是你的系统有用户的概念的话,通常来讲,你应该有一个用户表,最简单的用户表,应该有三列:Id,Username和Password,相似下表这种

ID USERNAME PASSWORD
10 wang abcdefg

并且不是全部用户都是一种角色,好比网站管理员、供应商、财务等等,这些角色和网站的直接用户须要的权限多是不同的。那么咱们就须要一个角色表:

ID ROLE
10 USER
20 ADMIN

固然咱们还须要一个能够将用户和角色关联起来创建映射关系的表。

USER_ID ROLE_ID
10 10
20 20

这是典型的一个关系型数据库的用户角色的设计,因为咱们要使用的MongoDB是一个文档型数据库,因此让咱们从新审视一下这个结构。

这个数据结构的优势在于它避免了数据的冗余,每一个表负责本身的数据,经过关联表进行关系的描述,同时也保证的数据的完整性:好比当你修改角色名称后,没有脏数据的产生。

可是这种事情在用户权限这个领域发生的频率到底有多少呢?有多少人天天不停的改的角色名称?固然若是你的业务场景确实是须要保证数据完整性,你仍是应该使用关系型数据库。但若是没有高频的对于角色表的改动,其实咱们是不须要这样的一个设计的。在MongoDB中咱们能够将其简化为

{
  _id: <id_generated>
  username: 'user', password: 'pass', roles: ['USER', 'ADMIN'] }

基于以上考虑,咱们重构一下 User 类,

@Data
public class User { @Id private String id; @Indexed(unique=true, direction= IndexDirection.DESCENDING, dropDups=true) private String username; private String password; private String email; private Date lastPasswordResetDate; private List<String> roles; }

固然你可能发现这个类有点怪,只有一些field,这个简化的能力是一个叫 lombok 类库提供的 ,这个不少开发过Android的童鞋应该熟悉,是用来简化POJO的建立的一个类库。简单说一下,采用 lombok 提供的 @Data 修饰符后能够简写成,原来的一坨getter和setter以及constructor等都不须要写了。相似的 Todo 能够改写成:

@Data
public class Todo { @Id private String id; private String desc; private boolean completed; private User user; }

增长这个类库只需在 build.gradle 中增长下面这行

dependencies {
    // 省略其它依赖 compile("org.projectlombok:lombok:${lombokVersion}") }

引入Spring Security

要在Spring Boot中引入Spring Security很是简单,修改 build.gradle ,增长一个引用 org.springframework.boot:spring-boot-starter-security :

dependencies {
    compile("org.springframework.boot:spring-boot-starter-data-rest") compile("org.springframework.boot:spring-boot-starter-data-mongodb") compile("org.springframework.boot:spring-boot-starter-security") compile("io.jsonwebtoken:jjwt:${jjwtVersion}") compile("org.projectlombok:lombok:${lombokVersion}") testCompile("org.springframework.boot:spring-boot-starter-test") }

你可能发现了,咱们不仅增长了对Spring Security的编译依赖,还增长 jjwt 的依赖。

Spring Security须要咱们实现几个东西,第一个是UserDetails:这个接口中规定了用户的几个必需要有的方法,因此咱们建立一个JwtUser类来实现这个接口。为何不直接使用User类?由于这个UserDetails彻底是为了安全服务的,它和咱们的领域类可能有部分属性重叠,但不少的接口实际上是安全定制的,因此最好新建一个类:

public class JwtUser implements UserDetails { private final String id; private final String username; private final String password; private final String email; private final Collection<? extends GrantedAuthority> authorities; private final Date lastPasswordResetDate; public JwtUser( String id, String username, String password, String email, Collection<? extends GrantedAuthority> authorities, Date lastPasswordResetDate) { this.id = id; this.username = username; this.password = password; this.email = email; this.authorities = authorities; this.lastPasswordResetDate = lastPasswordResetDate; } //返回分配给用户的角色列表 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @JsonIgnore public String getId() { return id; } @JsonIgnore @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } // 帐户是否未过时 @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } // 帐户是否未锁定 @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } // 密码是否未过时 @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } // 帐户是否激活 @JsonIgnore @Override public boolean isEnabled() { return true; } // 这个是自定义的,返回上次密码重置日期 @JsonIgnore public Date getLastPasswordResetDate() { return lastPasswordResetDate; } }

这个接口中规定的不少方法咱们都简单粗暴的设成直接返回某个值了,这是为了简单起见,你在实际开发环境中仍是要根据具体业务调整。固然因为两个类仍是有必定关系的,为了写起来简单,咱们写一个工厂类来由领域对象建立 JwtUser ,这个工厂就叫 JwtUserFactory 吧:

public final class JwtUserFactory { private JwtUserFactory() { } public static JwtUser create(User user) { return new JwtUser( user.getId(), user.getUsername(), user.getPassword(), user.getEmail(), mapToGrantedAuthorities(user.getRoles()), user.getLastPasswordResetDate() ); } private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) { return authorities.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } }

第二个要实现的是 UserDetailsService ,这个接口只定义了一个方法 loadUserByUsername ,顾名思义,就是提供一种从用户名能够查到用户并返回的方法。注意,不必定是数据库哦,文本文件、xml文件等等均可能成为数据源,这也是为何Spring提供这样一个接口的缘由:保证你能够采用灵活的数据源。接下来咱们创建一个 JwtUserDetailsServiceImpl 来实现这个接口。

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException(String.format("No user found with username '%s'.", username)); } else { return JwtUserFactory.create(user); } } }

为了让Spring能够知道咱们想怎样控制安全性,咱们还须要创建一个安全配置类 WebSecurityConfig :

@Configuration
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ // Spring会自动寻找一样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了 @Autowired private UserDetailsService userDetailsService; @Autowired public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder // 设置UserDetailsService .userDetailsService(this.userDetailsService) // 使用BCrypt进行密码的hash .passwordEncoder(passwordEncoder()); } // 装载BCrypt密码编码器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity // 因为使用的是JWT,咱们这里不须要csrf .csrf().disable() // 基于token,因此不须要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll() // 容许对于网站静态资源的无受权访问 .antMatchers( HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() // 对于获取token的rest api要容许匿名访问 .antMatchers("/auth/**").permitAll() // 除上面外的全部请求所有须要鉴权认证 .anyRequest().authenticated(); // 禁用缓存 httpSecurity.headers().cacheControl(); } }

接下来咱们要规定一下哪些资源须要什么样的角色能够访问了,在 UserController 加一个修饰符 @PreAuthorize("hasRole('ADMIN')") 表示这个资源只能被拥有 ADMIN 角色的用户访问。

/**
 * 在 @PreAuthorize 中咱们能够利用内建的 SPEL 表达式:好比 'hasRole()' 来决定哪些用户有权访问。
 * 需注意的一点是 hasRole 表达式认为每一个角色名字前都有一个前缀 'ROLE_'。因此这里的 'ADMIN' 其实在
 * 数据库中存储的是 'ROLE_ADMIN' 。这个 @PreAuthorize 能够修饰Controller也可修饰Controller中的方法。
 **/
@RestController @RequestMapping("/users") @PreAuthorize("hasRole('ADMIN')") public class UserController { @Autowired private UserRepository repository; @RequestMapping(method = RequestMethod.GET) public List<User> getUsers() { return repository.findAll(); } // 略去其它部分 }

相似的咱们给 TodoController 加上 @PreAuthorize("hasRole('USER')") ,标明这个资源只能被拥有 USER 角色的用户访问:

@RestController
@RequestMapping("/todos") @PreAuthorize("hasRole('USER')") public class TodoController { // 略去 }

使用application.yml配置SpringBoot应用

如今应该Spring Security能够工做了,但为了能够更清晰的看到工做日志,咱们但愿配置一下,在和 src 同级创建一个config文件夹,在这个文件夹下面新建一个 application.yml 。

# Server configuration
server:  port: 8090  contextPath: # Spring configuration spring:  jackson:  serialization:  INDENT_OUTPUT: true data.mongodb:  host: localhost  port: 27017  database: springboot # Logging configuration logging:  level: org.springframework:  data: DEBUG  security: DEBUG

咱们除了配置了logging的一些东东外,也顺手设置了数据库和http服务的一些配置项,如今咱们的服务器会在8090端口监听,而spring data和security的日志在debug模式下会输出到console。

如今启动服务后,访问 http://localhost:8090 你能够看到根目录仍是正常显示的

根目录仍是正常能够访问的

但咱们试一下 http://localhost:8090/users ,观察一下console,咱们会看到以下的输出,告诉因为用户未鉴权,咱们访问被拒绝了。

2017-03-10 15:51:53.351 DEBUG 57599 --- [nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter : Access is denied (user is anonymous); redirecting to authentication entry point org.springframework.security.access.AccessDeniedException: Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]

集成JWT和Spring Security

到如今,咱们仍是让JWT和Spring Security各自为战,并无集成起来。要想要JWT在Spring中工做,咱们应该新建一个filter,并把它配置在 WebSecurityConfig 中。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.header}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(tokenHead)) { final String authToken = authHeader.substring(tokenHead.length()); // The part after "Bearer " String username = jwtTokenUtil.getUsernameFromToken(authToken); logger.info("checking authentication " + username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); logger.info("authenticated user " + username + ", setting security context"); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }

事实上若是咱们足够相信token中的数据,也就是咱们足够相信签名token的secret的机制足够好,这种状况下,咱们能够不用再查询数据库,而直接采用token中的数据。本例中,咱们仍是经过Spring Security的 @UserDetailsService 进行了数据查询,但简单验证的话,你能够采用直接验证token是否合法来避免昂贵的数据查询。

接下来,咱们会在 WebSecurityConfig 中注入这个filter,而且配置到 HttpSecurity 中:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ // 省略其它部分 @Bean public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtAuthenticationTokenFilter(); } @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 省略以前写的规则部分,具体看前面的代码 // 添加JWT filter httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); } }

完成鉴权(登陆)、注册和更新token的功能

到如今,咱们整个API其实已经在安全的保护下了,但咱们遇到一个问题:全部的API都安全了,但咱们尚未用户啊,因此全部API都无法访问。所以要提供一个注册、登陆的API,这个API应该是能够匿名访问的。给它规划的路径呢,咱们前面其实在 WebSecurityConfig 中已经给出了,就是 /auth 。

首先须要一个AuthService,规定一下必选动做:

public interface AuthService { User register(User userToAdd); String login(String username, String password); String refresh(String oldToken); }

而后,实现这些必选动做,其实很是简单:

  1. 登陆时要生成token,完成Spring Security认证,而后返回token给客户端
  2. 注册时将用户密码用BCrypt加密,写入用户角色,因为是开放注册,因此写入角色系统控制,将其写成 ROLE_USER
  3. 提供一个能够刷新token的接口 refresh 用于取得新的token
@Service
public class AuthServiceImpl implements AuthService { private AuthenticationManager authenticationManager; private UserDetailsService userDetailsService; private JwtTokenUtil jwtTokenUtil; private UserRepository userRepository; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired public AuthServiceImpl( AuthenticationManager authenticationManager, UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, UserRepository userRepository) { this.authenticationManager = authenticationManager; this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.userRepository = userRepository; } @Override public User register(User userToAdd) { final String username = userToAdd.getUsername(); if(userRepository.findByUsername(username)!=null) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = userToAdd.getPassword(); userToAdd.setPassword(encoder.encode(rawPassword)); userToAdd.setLastPasswordResetDate(new Date()); userToAdd.setRoles(asList("ROLE_USER")); return userRepository.insert(userToAdd); } @Override public String login(String username, String password) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername(username); final String token = jwtTokenUtil.generateToken(userDetails); return token; } @Override public String refresh(String oldToken) { final String token = oldToken.substring(tokenHead.length()); String username = jwtTokenUtil.getUsernameFromToken(token); JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){ return jwtTokenUtil.refreshToken(token); } return null; } }

而后创建AuthController就好,这个AuthController中咱们在其中使用了表达式绑定,好比 @Value("${jwt.header}") 中的 jwt.header 实际上是定义在 applicaiton.yml 中的

# JWT
jwt:  header: Authorization  secret: mySecret  expiration: 604800  tokenHead: "Bearer "  route:  authentication:  path: auth  refresh: refresh  register: "auth/register"

一样的 @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定义在上面的

@RestController
public class AuthController { @Value("${jwt.header}") private String tokenHeader; @Autowired private AuthService authService; @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST) public ResponseEntity<?> createAuthenticationToken( @RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{ final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword()); // Return the token return ResponseEntity.ok(new JwtAuthenticationResponse(token)); } @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET) public ResponseEntity<?> refreshAndGetAuthenticationToken( HttpServletRequest request) throws AuthenticationException{ String token = request.getHeader(tokenHeader); String refreshedToken = authService.refresh(token); if(refreshedToken == null) { return ResponseEntity.badRequest().body(null); } else { return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken)); } } @RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST) public User register(@RequestBody User addedUser) throws AuthenticationException{ return authService.register(addedUser); } }

验证时间

接下来,咱们就能够看看咱们的成果了,首先注册一个用户 peng2 ,很完美的注册成功了

注册用户

而后在 /auth 中取得token,也很成功

取得token

不使用token时,访问 /users 的结果,不出意料的失败,提示未受权。

不使用token访问users列表

使用token时,访问 /users 的结果,虽然还是失败,但此次提示访问被拒绝,意思就是虽然你已经获得了受权,但因为你的会员级别还只是普卡会员,因此你的请求被拒绝。

image_1bas22va52vk1rj445fhm87k72a.png-156.9kB

接下来咱们访问 /users/?username=peng2 ,居然能够访问啊

访问本身的信息是容许的

这是因为咱们为这个方法定义的权限就是:拥有ADMIN角色或者是当前用户自己。Spring Security真是很方便,很强大。

@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')") @RequestMapping(value = "/",method = RequestMethod.GET) public User getUserByUsername(@RequestParam(value="username") String username) { return repository.findByUsername(username); }

本章代码: https://github.com/wpcfan/spring-boot-tut/tree/chap04

 

http://www.tuicool.com/articles/IVzuqaj

http://spring4all.com/question/93

https://tools.ietf.org/html/rfc7519#section-4.1

深刻浅出JWT(JSON Web Token )

1. JWT 介绍

JSON Web Token(JWT)是一个开放式标准(RFC 7519),它定义了一种紧凑(Compact)且自包含(Self-contained)的方式,用于在各方之间以JSON对象安全传输信息。 这些信息能够经过数字签名进行验证和信任。 可使用秘密(使用HMAC算法)或使用RSA的公钥/私钥对对JWT进行签名。

虽然JWT能够加密以提供各方之间的保密性,但咱们将重点关注已签名的令牌。 签名的令牌能够验证其中包含的索赔的完整性,而加密令牌隐藏来自其余方的索赔。 当令牌使用公钥/私钥对进行签名时,签名还证实只有持有私钥的方是签名方。

咱们来进一步解释一些概念:

  • Compact(紧凑)
    因为它们尺寸较小,JWT能够经过URL,POST参数或HTTP标头内发送。 另外,尺寸越小意味着传输速度越快。
  • Self-contained(自包含):
    有效载荷(Playload)包含有关用户的全部必需信息,避免了屡次查询数据库。

2. JWT适用场景

  • Authentication(鉴权)
    这是使用JWT最多见的状况。 一旦用户登陆,每一个后续请求都将包含JWT,容许用户访问该令牌容许的路由,服务和资源。 单点登陆是当今普遍使用JWT的一项功能,由于它的开销很小,而且可以轻松地跨不一样域使用。
  • Information Exchange(信息交换)
    JSON Web Tokens是在各方之间安全传输信息的好方式。 由于JWT能够签名:例如使用公钥/私钥对,因此能够肯定发件人是他们自称的人。 此外,因为使用标头和有效载荷计算签名,所以您还能够验证内容是否未被篡改。

3. JWT结构

在紧凑的形式中,JWT包含三个由点(.)分隔的部分,它们分别是:

  • Header
  • Payload
  • Signature

JWT结构一般以下所示:

xxxxx.yyyyy.zzzzz 

下面咱们分别来介绍这三个部分:

Header一般由两部分组成:令牌的类型,即JWT。和经常使用的散列算法,如HMAC SHA256或RSA。
例如:

{
  "alg": "HS256", "typ": "JWT" }

Header部分的JSON被Base64Url编码,造成JWT的第一部分。

Payload

这里放声明内容,能够说就是存放沟通信息的地方,在定义上有3种声明(Claims):

  • Registered claims(注册声明):
    这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操做的声明。 其中一些是:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。#Registered Claim Names#

  • Public claims(公开声明):
    这些能够由使用JWT的人员随意定义。 但为避免冲突,应在IANA JSON Web令牌注册表中定义它们,或将其定义为包含防冲突命名空间的URI。
  • Private claims(私有声明):
    这些是为了赞成使用它们可是既没有登记,也没有公开声明的各方之间共享信息,而建立的定制声明。

Playload示例以下:

{
  "sub": "1234567890", "name": "John Doe", "admin": true }

Playload部分的JSON被Base64Url编码,造成JWT的第二部分。

Notice:

请注意,对于已签名的令牌,此信息尽管受到篡改保护,但任何人均可以阅读。 除非加密,不然不要将秘密信息放在JWT的有效内容或标题元素中。这也是不少文章争论jwt安全性缘由,不要用 JWT 取代 Server-side 的 Session状态机制。详情请阅读这篇文章:Stop Using Jwt For Sessions.

Signature

第三部分signature用来验证发送请求者身份,由前两部分加密造成。
要建立签名部分,您必须采用编码标头,编码有效载荷,秘钥,标头中指定的算法并签名。
例如,若是你想使用HMAC SHA256算法,签名将按照如下方式建立:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

3. JWT实践

JWT输出的是三个由点分隔的Base64-URL字符串,能够在HTML和HTTP环境中轻松传递,而与基于XML的标准(如SAML)相比,它更加紧凑。

如下JWT示例,它具备先前的标头和有效负载编码,而且使用秘钥进行签名。

image

咱们可使用jwt.io调试器来解码,验证和生成JWT:
image

4.JWT工做原理

在身份验证中,当用户使用他们的凭证成功登陆时,JSON Web Token将被返回而且必须保存在本地(一般在本地存储中,但也可使用Cookie),而不是在传统方法中建立会话 服务器并返回一个cookie。

关于存储令牌(Token)的方式,必须考虑安全因素。
参考: #Where to Store Tokens#

不管什么时候用户想要访问受保护的路由或资源,用户代理都应使用承载方案发送JWT,一般在请求头中的Authorization字段,使用Bearer schema:

Authorization: Bearer <token>

这是一种无状态身份验证机制,由于用户状态永远不会保存在服务器内存中。 服务器受保护的路由将在受权头中检查有效的JWT,若是存在,则容许用户访问受保护的资源。 因为JWT是独立的,全部必要的信息都在那里,减小了屡次查询数据库的需求。

这使得咱们能够彻底依赖无状态的数据API,甚至向下游服务提出请求。 不管哪些域正在为API提供服务并不重要,所以不会出现跨域资源共享(CORS)的问题,由于它不使用Cookie。

image

Notice:

请注意,使用已签名的令牌,令牌中包含的全部信息都会暴露给用户或其余方,即便他们没法更改它。 在JWT中,不该该在Playload里面加入任何敏感的数据,好比像密码这样的内容。若是将用户的密码放在了JWT中,那么怀有恶意的第三方经过Base64解码就能很快地知道你的密码了。

5. 常见问题

① JWT 安全嗎?

Base64编码方式是可逆的,也就是透过编码后发放的Token内容是能够被解析的。通常而言,咱们都不建议在有效载荷内放敏感讯息,好比使用者的密码。

② JWT Payload 內容能够被伪造嗎?

JWT其中的一个组成内容为Signature,能够防止经过Base64可逆方法回推有效载荷内容并将其修改。由于Signature是经由Header跟Payload一块儿Base64组成的。

是的,Cookie丢失,就表示身份就能够被伪造。故官方建议的使用方式是存放在LocalStorage中,并放在请求头中发送。

④ 空间及长度问题?

JWT Token一般长度不会过小,特别是Stateless JWT Token,把全部的数据都编在Token里,很快的就会超过Cookie的大小(4K)或者是URL长度限制。

⑤ Token失效问题?

  • 无状态JWT令牌(Stateless JWT Token)发放出去以后,不能经过服务器端让令牌失效,必须等到过时时间过才会失去效用。
  • 假设在这之间Token被拦截,或者有权限管理身份的差别形成受权Scope修改,都不能阻止发出去的Token失效并要求使用者从新请求新的Token。

6. JWT使用建议

  • 不要存放敏感信息在Token里。
  • Payload中的exp时效不要设定太长。
  • 开启Only Http预防XSS攻击。
  • 若是担忧重播攻击(replay attacks )能够增长jti(JWT ID),exp(有效时间) Claim。
  • 在你的应用程序应用层中增长黑名单机制,必要的时候能够进行Block作阻挡(这是针对掉令牌被第三方使用窃取的手动防护)。

[1] Stop using JWT for sessions:
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
[3] Use JWT The Right Way!:
https://stormpath.com/blog/jwt-the-right-way
[2] JSON Web Token 维基百科:
https://en.wikipedia.org/wiki/JSON_Web_Token

https://www.cnblogs.com/mantoudev/p/8994341.html

相关文章
相关标签/搜索